diff --git a/bin/reportlab/graphics/__init__.py b/bin/reportlab/graphics/__init__.py new file mode 100644 index 00000000000..0e4597012a4 --- /dev/null +++ b/bin/reportlab/graphics/__init__.py @@ -0,0 +1,4 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/__init__.py +__version__=''' $Id: __init__.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' \ No newline at end of file diff --git a/bin/reportlab/graphics/barcode/README b/bin/reportlab/graphics/barcode/README new file mode 100644 index 00000000000..33cf57b7a66 --- /dev/null +++ b/bin/reportlab/graphics/barcode/README @@ -0,0 +1,59 @@ +Symbologies Currently Supported +=============================== + +The following have, at a minimum, been verified to scan with a WASP +CCD barcode scanner (found one bug in my code, two in the scanner!). +Some have had more extensive testing: + + Interleaved 2 of 5 + MSI + Codabar + Code 39 (Standard Character Set) + Code 39 (Extended Character Set) + Code 93 (Standard Character Set) + Code 93 (Extended Character Set) + Code 128 (Automatic use of A, B, C, with some optimizations -- + more coming) + +The following have been tested by sending a fair number of mailpieces +with them: + + USPS FIM + USPS POSTNET + +The following have not been tested, as none of the scanners I have +access to support them: + + Code 11 + + +Future Plans, Consulting +======================== + +Soon: + +I plan to implement the following linear codes soon: + + UPC/EAN(/JAN) + +The following are in progress, but I lack a way to verify them +(scanners I have access to don't read them), and I don't have complete +specs for the UK style. + + Royal Mail 4-State (UK/NL/etc style, and Australian style) + +Down the road, I'd like to do some 2D symbologies. Likely first candidate +is PDF417. MaxiCode, Aztec Code, and some of the stacked symbologies are +also good candidates. + +I am available to do implementation of additional symbologies for hire. +Because I enjoy hacking barcodes, my rates for work in this particular +area are very low and are mainly to help offset costs associated with +obtaining related documents and/or to buy or gain access to scanning +equipment for symbologies if I don't already have a scanner that +supports them. Loans of equipment are also accepted. + +For more information, contact: + +Ty Sarna +tsarna@sarna.org diff --git a/bin/reportlab/graphics/barcode/TODO b/bin/reportlab/graphics/barcode/TODO new file mode 100644 index 00000000000..5f2ffcb6699 --- /dev/null +++ b/bin/reportlab/graphics/barcode/TODO @@ -0,0 +1,24 @@ +See also README for some plans and info on consulting. + +- Overall framework docs + +- Finish Aussie Rules 4-State, for which I have complete docs now (yay + USPS and aupost.com.au for putting specs online. Too bad UKPost doesn't.) + +- Investigate USPS PLANET stuff + +- Higher-level objects that handle barcoded address blocks with correct + spacings and such (US, AU, UK/etc?) + +- Even higher-level objects that represent mailpieces and place the + above-style address block objects, FIM codes, "place stamp here" blocks, + etc, correctly? + +- Framework for laying out labels on various styles of n-up label + sheets, like Avery labels, etc? + +- Decide if Plessey is worth doing. MSI-like (MSI is actually derived from + it), but specs were never formalized. Probably only useful for legacy + applications. If you need it, mail me. + +- Get someone to test Code 11, or find a scanner that handles it diff --git a/bin/reportlab/graphics/barcode/VERSION b/bin/reportlab/graphics/barcode/VERSION new file mode 100644 index 00000000000..33ad517e76a --- /dev/null +++ b/bin/reportlab/graphics/barcode/VERSION @@ -0,0 +1 @@ +0.9 diff --git a/bin/reportlab/graphics/barcode/__init__.py b/bin/reportlab/graphics/barcode/__init__.py new file mode 100644 index 00000000000..bde067f504a --- /dev/null +++ b/bin/reportlab/graphics/barcode/__init__.py @@ -0,0 +1,126 @@ +# +# Copyright (c) 1996-2000 Tyler C. Sarna +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. All advertising materials mentioning features or use of this software +# must display the following acknowledgement: +# This product includes software developed by Tyler C. Sarna. +# 4. Neither the name of the author nor the names of contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +__version__ = '0.9' + +def getCodes(): + """Returns a dict mapping code names to widgets""" + + from widgets import (BarcodeI2of5, + BarcodeCode128, + BarcodeStandard93, + BarcodeExtended93, + BarcodeStandard39, + BarcodeExtended39, + BarcodeMSI, + BarcodeCodabar, + BarcodeCode11, + BarcodeFIM, + BarcodePOSTNET) + + #newer codes will typically get their own module + from eanbc import Ean13BarcodeWidget, Ean8BarcodeWidget + + + #the module exports a dictionary of names to widgets, to make it easy for + #apps and doc tools to display information about them. + codes = {} + for widget in ( + BarcodeI2of5, + BarcodeCode128, + BarcodeStandard93, + BarcodeExtended93, + BarcodeStandard39, + BarcodeExtended39, + BarcodeMSI, + BarcodeCodabar, + BarcodeCode11, + BarcodeFIM, + BarcodePOSTNET, + Ean13BarcodeWidget, + Ean8BarcodeWidget, + ): + codeName = widget.codeName + codes[codeName] = widget + + return codes + +def getCodeNames(): + """Returns sorted list of supported bar code names""" + return sorted(getCodes().keys()) + +def createBarcodeDrawing(codeName, **options): + """This creates and returns a drawing with a barcode. + """ + from reportlab.graphics.shapes import Drawing, Group + + codes = getCodes() + bcc = codes[codeName] + width = options.pop('width',None) + height = options.pop('height',None) + isoScale = options.pop('isoScale',0) + kw = {} + for k,v in options.iteritems(): + if k.startswith('_') or k in bcc._attrMap: kw[k] = v + bc = bcc(**kw) + + #size it after setting the data + x1, y1, x2, y2 = bc.getBounds() + w = float(x2 - x1) + h = float(y2 - y1) + sx = width not in ('auto',None) + sy = height not in ('auto',None) + if sx or sy: + sx = sx and width/w or 1.0 + sy = sy and height/h or 1.0 + if isoScale: + if sx<1.0 and sy<1.0: + sx = sy = max(sx,sy) + else: + sx = sy = min(sx,sy) + + w *= sx + h *= sy + else: + sx = sy = 1 + + #bc.x = -sx*x1 + #bc.y = -sy*y1 + d = Drawing(width=w,height=h,transform=[sx,0,0,sy,-sx*x1,-sy*y1]) + d.add(bc, "_bc") + return d + +def createBarcodeImageInMemory(codeName, **options): + """This creates and returns barcode as an image in memory. + """ + d = createBarcodeDrawing(codeName, **options) + return d.asString(format) diff --git a/bin/reportlab/graphics/barcode/code128.py b/bin/reportlab/graphics/barcode/code128.py new file mode 100644 index 00000000000..443edb06c84 --- /dev/null +++ b/bin/reportlab/graphics/barcode/code128.py @@ -0,0 +1,322 @@ +# +# Copyright (c) 2000 Tyler C. Sarna +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. All advertising materials mentioning features or use of this software +# must display the following acknowledgement: +# This product includes software developed by Tyler C. Sarna. +# 4. Neither the name of the author nor the names of contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +from reportlab.lib.units import inch +from common import MultiWidthBarcode +from string import digits + +_patterns = { + 0 : 'BaBbBb', 1 : 'BbBaBb', 2 : 'BbBbBa', + 3 : 'AbAbBc', 4 : 'AbAcBb', 5 : 'AcAbBb', + 6 : 'AbBbAc', 7 : 'AbBcAb', 8 : 'AcBbAb', + 9 : 'BbAbAc', 10 : 'BbAcAb', 11 : 'BcAbAb', + 12 : 'AaBbCb', 13 : 'AbBaCb', 14 : 'AbBbCa', + 15 : 'AaCbBb', 16 : 'AbCaBb', 17 : 'AbCbBa', + 18 : 'BbCbAa', 19 : 'BbAaCb', 20 : 'BbAbCa', + 21 : 'BaCbAb', 22 : 'BbCaAb', 23 : 'CaBaCa', + 24 : 'CaAbBb', 25 : 'CbAaBb', 26 : 'CbAbBa', + 27 : 'CaBbAb', 28 : 'CbBaAb', 29 : 'CbBbAa', + 30 : 'BaBaBc', 31 : 'BaBcBa', 32 : 'BcBaBa', + 33 : 'AaAcBc', 34 : 'AcAaBc', 35 : 'AcAcBa', + 36 : 'AaBcAc', 37 : 'AcBaAc', 38 : 'AcBcAa', + 39 : 'BaAcAc', 40 : 'BcAaAc', 41 : 'BcAcAa', + 42 : 'AaBaCc', 43 : 'AaBcCa', 44 : 'AcBaCa', + 45 : 'AaCaBc', 46 : 'AaCcBa', 47 : 'AcCaBa', + 48 : 'CaCaBa', 49 : 'BaAcCa', 50 : 'BcAaCa', + 51 : 'BaCaAc', 52 : 'BaCcAa', 53 : 'BaCaCa', + 54 : 'CaAaBc', 55 : 'CaAcBa', 56 : 'CcAaBa', + 57 : 'CaBaAc', 58 : 'CaBcAa', 59 : 'CcBaAa', + 60 : 'CaDaAa', 61 : 'BbAdAa', 62 : 'DcAaAa', + 63 : 'AaAbBd', 64 : 'AaAdBb', 65 : 'AbAaBd', + 66 : 'AbAdBa', 67 : 'AdAaBb', 68 : 'AdAbBa', + 69 : 'AaBbAd', 70 : 'AaBdAb', 71 : 'AbBaAd', + 72 : 'AbBdAa', 73 : 'AdBaAb', 74 : 'AdBbAa', + 75 : 'BdAbAa', 76 : 'BbAaAd', 77 : 'DaCaAa', + 78 : 'BdAaAb', 79 : 'AcDaAa', 80 : 'AaAbDb', + 81 : 'AbAaDb', 82 : 'AbAbDa', 83 : 'AaDbAb', + 84 : 'AbDaAb', 85 : 'AbDbAa', 86 : 'DaAbAb', + 87 : 'DbAaAb', 88 : 'DbAbAa', 89 : 'BaBaDa', + 90 : 'BaDaBa', 91 : 'DaBaBa', 92 : 'AaAaDc', + 93 : 'AaAcDa', 94 : 'AcAaDa', 95 : 'AaDaAc', + 96 : 'AaDcAa', 97 : 'DaAaAc', 98 : 'DaAcAa', + 99 : 'AaCaDa', 100 : 'AaDaCa', 101 : 'CaAaDa', + 102 : 'DaAaCa', 103 : 'BaAdAb', 104 : 'BaAbAd', + 105 : 'BaAbCb', 106 : 'BcCaAaB' +} + +starta, startb, startc, stop = 103, 104, 105, 106 + +seta = { + ' ' : 0, '!' : 1, '"' : 2, '#' : 3, + '$' : 4, '%' : 5, '&' : 6, '\'' : 7, + '(' : 8, ')' : 9, '*' : 10, '+' : 11, + ',' : 12, '-' : 13, '.' : 14, '/' : 15, + '0' : 16, '1' : 17, '2' : 18, '3' : 19, + '4' : 20, '5' : 21, '6' : 22, '7' : 23, + '8' : 24, '9' : 25, ':' : 26, ';' : 27, + '<' : 28, '=' : 29, '>' : 30, '?' : 31, + '@' : 32, 'A' : 33, 'B' : 34, 'C' : 35, + 'D' : 36, 'E' : 37, 'F' : 38, 'G' : 39, + 'H' : 40, 'I' : 41, 'J' : 42, 'K' : 43, + 'L' : 44, 'M' : 45, 'N' : 46, 'O' : 47, + 'P' : 48, 'Q' : 49, 'R' : 50, 'S' : 51, + 'T' : 52, 'U' : 53, 'V' : 54, 'W' : 55, + 'X' : 56, 'Y' : 57, 'Z' : 58, '[' : 59, + '\\' : 60, ']' : 61, '^' : 62, '_' : 63, + '\x00' : 64, '\x01' : 65, '\x02' : 66, '\x03' : 67, + '\x04' : 68, '\x05' : 69, '\x06' : 70, '\x07' : 71, + '\x08' : 72, '\x09' : 73, '\x0a' : 74, '\x0b' : 75, + '\x0c' : 76, '\x0d' : 77, '\x0e' : 78, '\x0f' : 79, + '\x10' : 80, '\x11' : 81, '\x12' : 82, '\x13' : 83, + '\x14' : 84, '\x15' : 85, '\x16' : 86, '\x17' : 87, + '\x18' : 88, '\x19' : 89, '\x1a' : 90, '\x1b' : 91, + '\x1c' : 92, '\x1d' : 93, '\x1e' : 94, '\x1f' : 95, + '\xf3' : 96, '\xf2' : 97, 'SHIFT' : 98, 'TO_C' : 99, + 'TO_B' : 100, '\xf4' : 101, '\xf1' : 102 +} + +setb = { + ' ' : 0, '!' : 1, '"' : 2, '#' : 3, + '$' : 4, '%' : 5, '&' : 6, '\'' : 7, + '(' : 8, ')' : 9, '*' : 10, '+' : 11, + ',' : 12, '-' : 13, '.' : 14, '/' : 15, + '0' : 16, '1' : 17, '2' : 18, '3' : 19, + '4' : 20, '5' : 21, '6' : 22, '7' : 23, + '8' : 24, '9' : 25, ':' : 26, ';' : 27, + '<' : 28, '=' : 29, '>' : 30, '?' : 31, + '@' : 32, 'A' : 33, 'B' : 34, 'C' : 35, + 'D' : 36, 'E' : 37, 'F' : 38, 'G' : 39, + 'H' : 40, 'I' : 41, 'J' : 42, 'K' : 43, + 'L' : 44, 'M' : 45, 'N' : 46, 'O' : 47, + 'P' : 48, 'Q' : 49, 'R' : 50, 'S' : 51, + 'T' : 52, 'U' : 53, 'V' : 54, 'W' : 55, + 'X' : 56, 'Y' : 57, 'Z' : 58, '[' : 59, + '\\' : 60, ']' : 61, '^' : 62, '_' : 63, + '`' : 64, 'a' : 65, 'b' : 66, 'c' : 67, + 'd' : 68, 'e' : 69, 'f' : 70, 'g' : 71, + 'h' : 72, 'i' : 73, 'j' : 74, 'k' : 75, + 'l' : 76, 'm' : 77, 'n' : 78, 'o' : 79, + 'p' : 80, 'q' : 81, 'r' : 82, 's' : 83, + 't' : 84, 'u' : 85, 'v' : 86, 'w' : 87, + 'x' : 88, 'y' : 89, 'z' : 90, '{' : 91, + '|' : 92, '}' : 93, '~' : 94, '\x7f' : 95, + '\xf3' : 96, '\xf2' : 97, 'SHIFT' : 98, 'TO_C' : 99, + '\xf4' : 100, 'TO_A' : 101, '\xf1' : 102 +} + +setc = { + '00': 0, '01': 1, '02': 2, '03': 3, '04': 4, + '05': 5, '06': 6, '07': 7, '08': 8, '09': 9, + '10':10, '11':11, '12':12, '13':13, '14':14, + '15':15, '16':16, '17':17, '18':18, '19':19, + '20':20, '21':21, '22':22, '23':23, '24':24, + '25':25, '26':26, '27':27, '28':28, '29':29, + '30':30, '31':31, '32':32, '33':33, '34':34, + '35':35, '36':36, '37':37, '38':38, '39':39, + '40':40, '41':41, '42':42, '43':43, '44':44, + '45':45, '46':46, '47':47, '48':48, '49':49, + '50':50, '51':51, '52':52, '53':53, '54':54, + '55':55, '56':56, '57':57, '58':58, '59':59, + '60':60, '61':61, '62':62, '63':63, '64':64, + '65':65, '66':66, '67':67, '68':68, '69':69, + '70':70, '71':71, '72':72, '73':73, '74':74, + '75':75, '76':76, '77':77, '78':78, '79':79, + '80':80, '81':81, '82':82, '83':83, '84':84, + '85':85, '86':86, '87':87, '88':88, '89':89, + '90':90, '91':91, '92':92, '93':93, '94':94, + '95':95, '96':96, '97':97, '98':98, '99':99, + + 'TO_B' : 100, 'TO_A' : 101, '\xf1' : 102 +} + +setmap = { + 'TO_A' : (seta, setb), + 'TO_B' : (setb, seta), + 'TO_C' : (setc, None), + 'START_A' : (starta, seta, setb), + 'START_B' : (startb, setb, seta), + 'START_C' : (startc, setc, None), +} +tos = setmap.keys() + +class Code128(MultiWidthBarcode): + """ + Code 128 is a very compact symbology that can encode the entire + 128 character ASCII set, plus 4 special control codes, + (FNC1-FNC4, expressed in the input string as \xf1 to \xf4). + Code 128 can also encode digits at double density (2 per byte) + and has a mandatory checksum. Code 128 is well supported and + commonly used -- for example, by UPS for tracking labels. + + Because of these qualities, Code 128 is probably the best choice + for a linear symbology today (assuming you have a choice). + + Options that may be passed to constructor: + + value (int, or numeric string. required.): + The value to encode. + + barWidth (float, default .0075): + X-Dimension, or width of the smallest element + Minumum is .0075 inch (7.5 mils). + + barHeight (float, see default below): + Height of the symbol. Default is the height of the two + bearer bars (if they exist) plus the greater of .25 inch + or .15 times the symbol's length. + + quiet (bool, default 1): + Wether to include quiet zones in the symbol. + + lquiet (float, see default below): + Quiet zone size to left of code, if quiet is true. + Default is the greater of .25 inch, or 10 barWidth + + rquiet (float, defaults as above): + Quiet zone size to right left of code, if quiet is true. + + Sources of Information on Code 128: + + http://www.semiconductor.agilent.com/barcode/sg/Misc/code_128.html + http://www.adams1.com/pub/russadam/128code.html + http://www.barcodeman.com/c128.html + + Official Spec, "ANSI/AIM BC4-1999, ISS" is available for US$45 from + http://www.aimglobal.org/aimstore/ + """ + barWidth = inch * 0.0075 + lquiet = None + rquiet = None + quiet = 1 + barHeight = None + def __init__(self, value='', **args): + + if type(value) is type(1): + value = str(value) + + for (k, v) in args.items(): + setattr(self, k, v) + + if self.quiet: + if self.lquiet is None: + self.lquiet = max(inch * 0.25, self.barWidth * 10.0) + if self.rquiet is None: + self.rquiet = max(inch * 0.25, self.barWidth * 10.0) + else: + self.lquiet = self.rquiet = 0.0 + + MultiWidthBarcode.__init__(self, value) + + def validate(self): + vval = "" + self.valid = 1 + for c in self.value: + if ord(c) > 127 and c not in '\xf1\xf2\xf3\xf4': + self.valid = 0 + continue + vval = vval + c + self.validated = vval + return vval + + def _trailingDigitsToC(self, l): + # Optimization: trailing digits -> set C double-digits + c = 1 + savings = -1 # the TO_C costs one character + rl = ['STOP'] + while c < len(l): + i = (-c - 1) + if l[i] == '\xf1': + c = c + 1 + rl.insert(0, '\xf1') + continue + elif len(l[i]) == 1 and l[i] in digits \ + and len(l[i-1]) == 1 and l[i-1] in digits: + c += 2 + savings += 1 + rl.insert(0, l[i-1] + l[i]) + continue + else: + break + if savings > 0: + return l[:-c] + ['TO_C'] + rl + else: + return l + + def encode(self): + # First, encode using only B + s = self.validated + l = ['START_B'] + for c in s: + if not setb.has_key(c): + l = l + ['TO_A', c, 'TO_B'] + else: + l.append(c) + l.append('STOP') + + l = self._trailingDigitsToC(l) + + # Finally, replace START_X,TO_Y with START_Y + if l[1] in tos: + l[:2] = ['START_' + l[1][-1]] + +# print `l` + + # encode into numbers + start, set, shset = setmap[l[0]] + e = [start] + + l = l[1:-1] + while l: + c = l[0] + if c == 'SHIFT': + e = e + [set[c], shset[l[1]]] + l = l[2:] + elif c in tos: + e.append(set[c]) + set, shset = setmap[c] + l = l[1:] + else: + e.append(set[c]) + l = l[1:] + + c = e[0] + for i in range(1, len(e)): + c = c + i * e[i] + self.encoded = e + [c % 103, stop] + return self.encoded + + def decompose(self): + self.decomposed = ''.join([_patterns[c] for c in self.encoded]) + return self.decomposed + + def _humanText(self): + return self.value diff --git a/bin/reportlab/graphics/barcode/code39.py b/bin/reportlab/graphics/barcode/code39.py new file mode 100644 index 00000000000..697b54045b7 --- /dev/null +++ b/bin/reportlab/graphics/barcode/code39.py @@ -0,0 +1,248 @@ +# +# Copyright (c) 1996-2000 Tyler C. Sarna +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. All advertising materials mentioning features or use of this software +# must display the following acknowledgement: +# This product includes software developed by Tyler C. Sarna. +# 4. Neither the name of the author nor the names of contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +from reportlab.lib.units import inch +from common import Barcode +import string + +_patterns = { + '0': ("bsbSBsBsb", 0), '1': ("BsbSbsbsB", 1), + '2': ("bsBSbsbsB", 2), '3': ("BsBSbsbsb", 3), + '4': ("bsbSBsbsB", 4), '5': ("BsbSBsbsb", 5), + '6': ("bsBSBsbsb", 6), '7': ("bsbSbsBsB", 7), + '8': ("BsbSbsBsb", 8), '9': ("bsBSbsBsb", 9), + 'A': ("BsbsbSbsB", 10), 'B': ("bsBsbSbsB", 11), + 'C': ("BsBsbSbsb", 12), 'D': ("bsbsBSbsB", 13), + 'E': ("BsbsBSbsb", 14), 'F': ("bsBsBSbsb", 15), + 'G': ("bsbsbSBsB", 16), 'H': ("BsbsbSBsb", 17), + 'I': ("bsBsbSBsb", 18), 'J': ("bsbsBSBsb", 19), + 'K': ("BsbsbsbSB", 20), 'L': ("bsBsbsbSB", 21), + 'M': ("BsBsbsbSb", 22), 'N': ("bsbsBsbSB", 23), + 'O': ("BsbsBsbSb", 24), 'P': ("bsBsBsbSb", 25), + 'Q': ("bsbsbsBSB", 26), 'R': ("BsbsbsBSb", 27), + 'S': ("bsBsbsBSb", 28), 'T': ("bsbsBsBSb", 29), + 'U': ("BSbsbsbsB", 30), 'V': ("bSBsbsbsB", 31), + 'W': ("BSBsbsbsb", 32), 'X': ("bSbsBsbsB", 33), + 'Y': ("BSbsBsbsb", 34), 'Z': ("bSBsBsbsb", 35), + '-': ("bSbsbsBsB", 36), '.': ("BSbsbsBsb", 37), + ' ': ("bSBsbsBsb", 38), '*': ("bSbsBsBsb", 39), + '$': ("bSbSbSbsb", 40), '/': ("bSbSbsbSb", 41), + '+': ("bSbsbSbSb", 42), '%': ("bsbSbSbSb", 43) +} + +_valchars = [ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', + 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', + 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', + 'X', 'Y', 'Z', '-', '.', ' ', '*', '$', '/', '+', '%' +] + +_extended = { + '\0': "%U", '\01': "$A", '\02': "$B", '\03': "$C", + '\04': "$D", '\05': "$E", '\06': "$F", '\07': "$G", + '\010': "$H", '\011': "$I", '\012': "$J", '\013': "$K", + '\014': "$L", '\015': "$M", '\016': "$N", '\017': "$O", + '\020': "$P", '\021': "$Q", '\022': "$R", '\023': "$S", + '\024': "$T", '\025': "$U", '\026': "$V", '\027': "$W", + '\030': "$X", '\031': "$Y", '\032': "$Z", '\033': "%A", + '\034': "%B", '\035': "%C", '\036': "%D", '\037': "%E", + '!': "/A", '"': "/B", '#': "/C", '$': "/D", + '%': "/E", '&': "/F", '\'': "/G", '(': "/H", + ')': "/I", '*': "/J", '+': "/K", ',': "/L", + '/': "/O", ':': "/Z", ';': "%F", '<': "%G", + '=': "%H", '>': "%I", '?': "%J", '@': "%V", + '[': "%K", '\\': "%L", ']': "%M", '^': "%N", + '_': "%O", '`': "%W", 'a': "+A", 'b': "+B", + 'c': "+C", 'd': "+D", 'e': "+E", 'f': "+F", + 'g': "+G", 'h': "+H", 'i': "+I", 'j': "+J", + 'k': "+K", 'l': "+L", 'm': "+M", 'n': "+N", + 'o': "+O", 'p': "+P", 'q': "+Q", 'r': "+R", + 's': "+S", 't': "+T", 'u': "+U", 'v': "+V", + 'w': "+W", 'x': "+X", 'y': "+Y", 'z': "+Z", + '{': "%P", '|': "%Q", '}': "%R", '~': "%S", + '\177': "%T" +} + + +_stdchrs = string.digits + string.uppercase + "-. *$/+%" +_extchrs = _stdchrs + string.lowercase + \ + "\000\001\002\003\004\005\006\007\010\011\012\013\014\015\016\017" + \ + "\020\021\022\023\024\025\026\027\030\031\032\033\034\035\036\037" + \ + "!'#&\"(),:;<=>?@[\\]^_`{|}~\177" + +def _encode39(value, cksum, stop): + v = sum([_patterns[c][1] for c in value]) % 43 + if cksum: + value += _valchars[v] + if stop: value = '*'+value+'*' + return value + +class _Code39Base(Barcode): + barWidth = inch * 0.0075 + lquiet = None + rquiet = None + quiet = 1 + gap = None + barHeight = None + ratio = 2.2 + checksum = 1 + bearers = 0.0 + stop = 1 + def __init__(self, value = "", **args): + for k, v in args.iteritems(): + setattr(self, k, v) + + if self.quiet: + if self.lquiet is None: + self.lquiet = max(inch * 0.25, self.barWidth * 10.0) + self.rquiet = max(inch * 0.25, self.barWidth * 10.0) + else: + self.lquiet = self.rquiet = 0.0 + + Barcode.__init__(self, value) + + def decompose(self): + dval = "" + for c in self.encoded: + dval = dval + _patterns[c][0] + 'i' + self.decomposed = dval[:-1] + return self.decomposed + + def _humanText(self): + return self.stop and self.encoded[1:-1] or self.encoded + +class Standard39(_Code39Base): + """ + Options that may be passed to constructor: + + value (int, or numeric string. required.): + The value to encode. + + barWidth (float, default .0075): + X-Dimension, or width of the smallest element + Minumum is .0075 inch (7.5 mils). + + ratio (float, default 2.2): + The ratio of wide elements to narrow elements. + Must be between 2.0 and 3.0 (or 2.2 and 3.0 if the + barWidth is greater than 20 mils (.02 inch)) + + gap (float or None, default None): + width of intercharacter gap. None means "use barWidth". + + barHeight (float, see default below): + Height of the symbol. Default is the height of the two + bearer bars (if they exist) plus the greater of .25 inch + or .15 times the symbol's length. + + checksum (bool, default 1): + Wether to compute and include the check digit + + bearers (float, in units of barWidth. default 0): + Height of bearer bars (horizontal bars along the top and + bottom of the barcode). Default is 0 (no bearers). + + quiet (bool, default 1): + Wether to include quiet zones in the symbol. + + lquiet (float, see default below): + Quiet zone size to left of code, if quiet is true. + Default is the greater of .25 inch, or .15 times the symbol's + length. + + rquiet (float, defaults as above): + Quiet zone size to right left of code, if quiet is true. + + stop (bool, default 1): + Whether to include start/stop symbols. + + Sources of Information on Code 39: + + http://www.semiconductor.agilent.com/barcode/sg/Misc/code_39.html + http://www.adams1.com/pub/russadam/39code.html + http://www.barcodeman.com/c39_1.html + + Official Spec, "ANSI/AIM BC1-1995, USS" is available for US$45 from + http://www.aimglobal.org/aimstore/ + """ + def validate(self): + vval = "" + self.valid = 1 + for c in self.value: + if c in string.lowercase: + c = string.upper(c) + if c not in _stdchrs: + self.valid = 0 + continue + vval = vval + c + self.validated = vval + return vval + + def encode(self): + self.encoded = _encode39(self.validated, self.checksum, self.stop) + return self.encoded + +class Extended39(_Code39Base): + """ + Extended Code 39 is a convention for encoding additional characters + not present in stanmdard Code 39 by using pairs of characters to + represent the characters missing in Standard Code 39. + + See Standard39 for arguments. + + Sources of Information on Extended Code 39: + + http://www.semiconductor.agilent.com/barcode/sg/Misc/xcode_39.html + http://www.barcodeman.com/c39_ext.html + """ + def validate(self): + vval = "" + self.valid = 1 + for c in self.value: + if c not in _extchrs: + self.valid = 0 + continue + vval = vval + c + self.validated = vval + return vval + + def encode(self): + self.encoded = "" + for c in self.validated: + if _extended.has_key(c): + self.encoded = self.encoded + _extended[c] + elif c in _stdchrs: + self.encoded = self.encoded + c + else: + raise ValueError + self.encoded = _encode39(self.encoded, self.checksum,self.stop) + return self.encoded diff --git a/bin/reportlab/graphics/barcode/code93.py b/bin/reportlab/graphics/barcode/code93.py new file mode 100644 index 00000000000..0ab52e945e3 --- /dev/null +++ b/bin/reportlab/graphics/barcode/code93.py @@ -0,0 +1,236 @@ +# +# Copyright (c) 2000 Tyler C. Sarna +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. All advertising materials mentioning features or use of this software +# must display the following acknowledgement: +# This product includes software developed by Tyler C. Sarna. +# 4. Neither the name of the author nor the names of contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +from reportlab.lib.units import inch +from common import MultiWidthBarcode +import string + +_patterns = { + '0' : ('AcAaAb', 0), '1' : ('AaAbAc', 1), '2' : ('AaAcAb', 2), + '3' : ('AaAdAa', 3), '4' : ('AbAaAc', 4), '5' : ('AbAbAb', 5), + '6' : ('AbAcAa', 6), '7' : ('AaAaAd', 7), '8' : ('AcAbAa', 8), + '9' : ('AdAaAa', 9), 'A' : ('BaAaAc', 10), 'B' : ('BaAbAb', 11), + 'C' : ('BaAcAa', 12), 'D' : ('BbAaAb', 13), 'E' : ('BbAbAa', 14), + 'F' : ('BcAaAa', 15), 'G' : ('AaBaAc', 16), 'H' : ('AaBbAb', 17), + 'I' : ('AaBcAa', 18), 'J' : ('AbBaAb', 19), 'K' : ('AcBaAa', 20), + 'L' : ('AaAaBc', 21), 'M' : ('AaAbBb', 22), 'N' : ('AaAcBa', 23), + 'O' : ('AbAaBb', 24), 'P' : ('AcAaBa', 25), 'Q' : ('BaBaAb', 26), + 'R' : ('BaBbAa', 27), 'S' : ('BaAaBb', 28), 'T' : ('BaAbBa', 29), + 'U' : ('BbAaBa', 30), 'V' : ('BbBaAa', 31), 'W' : ('AaBaBb', 32), + 'X' : ('AaBbBa', 33), 'Y' : ('AbBaBa', 34), 'Z' : ('AbCaAa', 35), + '-' : ('AbAaCa', 36), '.' : ('CaAaAb', 37), ' ' : ('CaAbAa', 38), + '$' : ('CbAaAa', 39), '/' : ('AaBaCa', 40), '+' : ('AaCaBa', 41), + '%' : ('BaAaCa', 42), '#' : ('AbAbBa', 43), '!' : ('CaBaAa', 44), + '=' : ('CaAaBa', 45), '&' : ('AbBbAa', 46), + 'start' : ('AaAaDa', -1), 'stop' : ('AaAaDaA', -2) +} + +_charsbyval = {} +for k, v in _patterns.items(): + _charsbyval[v[1]] = k + +_extended = { + '\x00' : '!U', '\x01' : '#A', '\x02' : '#B', '\x03' : '#C', + '\x04' : '#D', '\x05' : '#E', '\x06' : '#F', '\x07' : '#G', + '\x08' : '#H', '\x09' : '#I', '\x0a' : '#J', '\x0b' : '#K', + '\x0c' : '#L', '\x0d' : '#M', '\x0e' : '#N', '\x0f' : '#O', + '\x10' : '#P', '\x11' : '#Q', '\x12' : '#R', '\x13' : '#S', + '\x14' : '#T', '\x15' : '#U', '\x16' : '#V', '\x17' : '#W', + '\x18' : '#X', '\x19' : '#Y', '\x1a' : '#Z', '\x1b' : '!A', + '\x1c' : '!B', '\x1d' : '!C', '\x1e' : '!D', '\x1f' : '!E', + '!' : '=A', '"' : '=B', '#' : '=C', '$' : '=D', + '%' : '=E', '&' : '=F', '\'' : '=G', '(' : '=H', + ')' : '=I', '*' : '=J', '+' : '=K', ',' : '=L', + '/' : '=O', ':' : '=Z', ';' : '!F', '<' : '!G', + '=' : '!H', '>' : '!I', '?' : '!J', '@' : '!V', + '[' : '!K', '\\' : '!L', ']' : '!M', '^' : '!N', + '_' : '!O', '`' : '!W', 'a' : '&A', 'b' : '&B', + 'c' : '&C', 'd' : '&D', 'e' : '&E', 'f' : '&F', + 'g' : '&G', 'h' : '&H', 'i' : '&I', 'j' : '&J', + 'k' : '&K', 'l' : '&L', 'm' : '&M', 'n' : '&N', + 'o' : '&O', 'p' : '&P', 'q' : '&Q', 'r' : '&R', + 's' : '&S', 't' : '&T', 'u' : '&U', 'v' : '&V', + 'w' : '&W', 'x' : '&X', 'y' : '&Y', 'z' : '&Z', + '{' : '!P', '|' : '!Q', '}' : '!R', '~' : '!S', + '\x7f' : '!T' +} + +def _encode93(str): + s = map(None, str) + s.reverse() + + # compute 'C' checksum + i = 0; v = 1; c = 0 + while i < len(s): + c = c + v * _patterns[s[i]][1] + i = i + 1; v = v + 1 + if v > 20: + v = 1 + s.insert(0, _charsbyval[c % 47]) + + # compute 'K' checksum + i = 0; v = 1; c = 0 + while i < len(s): + c = c + v * _patterns[s[i]][1] + i = i + 1; v = v + 1 + if v > 15: + v = 1 + s.insert(0, _charsbyval[c % 47]) + + s.reverse() + + return string.join(s, '') + +class _Code93Base(MultiWidthBarcode): + barWidth = inch * 0.0075 + lquiet = None + rquiet = None + quiet = 1 + barHeight = None + stop = 1 + def __init__(self, value='', **args): + + if type(value) is type(1): + value = str(value) + + for (k, v) in args.iteritems(): + setattr(self, k, v) + + if self.quiet: + if self.lquiet is None: + self.lquiet = max(inch * 0.25, self.barWidth * 10.0) + self.rquiet = max(inch * 0.25, self.barWidth * 10.0) + else: + self.lquiet = self.rquiet = 0.0 + + MultiWidthBarcode.__init__(self, value) + + def decompose(self): + dval = self.stop and [_patterns['start'][0]] or [] + dval += [_patterns[c][0] for c in self.encoded] + if self.stop: dval.append(_patterns['stop'][0]) + self.decomposed = ''.join(dval) + return self.decomposed + +class Standard93(_Code93Base): + """ + Code 93 is a Uppercase alphanumeric symbology with some punctuation. + See Extended Code 93 for a variant that can represent the entire + 128 characrter ASCII set. + + Options that may be passed to constructor: + + value (int, or numeric string. required.): + The value to encode. + + barWidth (float, default .0075): + X-Dimension, or width of the smallest element + Minumum is .0075 inch (7.5 mils). + + barHeight (float, see default below): + Height of the symbol. Default is the height of the two + bearer bars (if they exist) plus the greater of .25 inch + or .15 times the symbol's length. + + quiet (bool, default 1): + Wether to include quiet zones in the symbol. + + lquiet (float, see default below): + Quiet zone size to left of code, if quiet is true. + Default is the greater of .25 inch, or 10 barWidth + + rquiet (float, defaults as above): + Quiet zone size to right left of code, if quiet is true. + + stop (bool, default 1): + Whether to include start/stop symbols. + + Sources of Information on Code 93: + + http://www.semiconductor.agilent.com/barcode/sg/Misc/code_93.html + + Official Spec, "NSI/AIM BC5-1995, USS" is available for US$45 from + http://www.aimglobal.org/aimstore/ + """ + def validate(self): + vval = "" + self.valid = 1 + for c in self.value: + if c in string.lowercase: + c = string.upper(c) + if not _patterns.has_key(c): + self.valid = 0 + continue + vval = vval + c + self.validated = vval + return vval + + def encode(self): + self.encoded = _encode93(self.validated) + return self.encoded + + +class Extended93(_Code93Base): + """ + Extended Code 93 is a convention for encoding the entire 128 character + set using pairs of characters to represent the characters missing in + Standard Code 93. It is very much like Extended Code 39 in that way. + + See Standard93 for arguments. + """ + + def validate(self): + vval = [] + self.valid = 1 + a = vval.append + for c in self.value: + if not _patterns.has_key(c) and not _extended.has_key(c): + self.valid = 0 + continue + a(c) + self.validated = ''.join(vval) + return self.validated + + def encode(self): + self.encoded = "" + for c in self.validated: + if _patterns.has_key(c): + self.encoded = self.encoded + c + elif _extended.has_key(c): + self.encoded = self.encoded + _extended[c] + else: + raise ValueError + self.encoded = _encode93(self.encoded) + return self.encoded + + def _humanText(self): + return self.validated+self.encoded[-2:] diff --git a/bin/reportlab/graphics/barcode/common.py b/bin/reportlab/graphics/barcode/common.py new file mode 100644 index 00000000000..70dbcba72b6 --- /dev/null +++ b/bin/reportlab/graphics/barcode/common.py @@ -0,0 +1,748 @@ +# +# Copyright (c) 1996-2000 Tyler C. Sarna +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. All advertising materials mentioning features or use of this software +# must display the following acknowledgement: +# This product includes software developed by Tyler C. Sarna. +# 4. Neither the name of the author nor the names of contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +from reportlab.platypus.flowables import Flowable +from reportlab.lib.units import inch +import string + +class Barcode(Flowable): + """Abstract Base for barcodes. Includes implementations of + some methods suitable for the more primitive barcode types""" + + fontName = 'Courier' + fontSize = 12 + humanReadable = 0 + + def _humanText(self): + return self.encoded + + def __init__(self, value='',**args): + self.value = value + + for (k, v) in args.items(): + setattr(self, k, v) + + + if not hasattr(self, 'gap'): + self.gap = None + + self.validate() + self.encode() + self.decompose() + self.computeSize() + + def validate(self): + self.valid = 1 + self.validated = self.value + + def encode(self): + self.encoded = self.validated + + def decompose(self): + self.decomposed = self.encoded + + def computeSize(self, *args): + barWidth = self.barWidth + wx = barWidth * self.ratio + + if self.gap == None: + self.gap = barWidth + + w = 0.0 + + for c in self.decomposed: + if c in 'sb': + w = w + barWidth + elif c in 'SB': + w = w + wx + else: # 'i' + w = w + self.gap + + if self.barHeight is None: + self.barHeight = w * 0.15 + self.barHeight = max(0.25 * inch, self.barHeight) + if self.bearers: + self.barHeight = self.barHeight + self.bearers * 2.0 * barWidth + + if self.quiet: + w += self.lquiet + self.rquiet + + self.height = self.barHeight + self.width = w + + def draw(self): + barWidth = self.barWidth + wx = barWidth * self.ratio + + left = self.quiet and self.lquiet or 0 + b = self.bearers * barWidth + bb = b * 0.5 + tb = self.barHeight - (b * 1.5) + + for c in self.decomposed: + if c == 'i': + left = left + self.gap + elif c == 's': + left = left + barWidth + elif c == 'S': + left = left + wx + elif c == 'b': + self.rect(left, bb, barWidth, tb) + left = left + barWidth + elif c == 'B': + self.rect(left, bb, wx, tb) + left = left + wx + + if self.bearers: + self.rect(self.lquiet, 0, \ + self.width - (self.lquiet + self.rquiet), b) + self.rect(self.lquiet, self.barHeight - b, \ + self.width - (self.lquiet + self.rquiet), b) + + self.drawHumanReadable() + + def drawHumanReadable(self): + if self.humanReadable: + #we have text + from reportlab.pdfbase.pdfmetrics import getAscent, stringWidth + s = str(self._humanText()) + fontSize = self.fontSize + fontName = self.fontName + w = stringWidth(s,fontName,fontSize) + width = self.width + if self.quiet: + width -= self.lquiet+self.rquiet + x = self.lquiet + else: + x = 0 + if w>width: fontSize *= width/float(w) + y = 1.07*getAscent(fontName)*fontSize/1000. + self.annotate(x+width/2.,-y,s,fontName,fontSize) + + def rect(self, x, y, w, h): + self.canv.rect(x, y, w, h, stroke=0, fill=1) + + def annotate(self,x,y,text,fontName,fontSize,anchor='middle'): + canv = self.canv + canv.saveState() + canv.setFont(self.fontName,fontSize) + if anchor=='middle': func = 'drawCentredString' + elif anchor=='end': func = 'drawRightString' + else: func = 'drawString' + getattr(canv,func)(text,x,y) + canv.restoreState() + +class MultiWidthBarcode(Barcode): + """Base for variable-bar-width codes like Code93 and Code128""" + + def computeSize(self, *args): + barWidth = self.barWidth + oa, oA = ord('a') - 1, ord('A') - 1 + + w = 0.0 + + for c in self.decomposed: + oc = ord(c) + if c in string.lowercase: + w = w + barWidth * (oc - oa) + elif c in string.uppercase: + w = w + barWidth * (oc - oA) + + if self.barHeight is None: + self.barHeight = w * 0.15 + self.barHeight = max(0.25 * inch, self.barHeight) + + if self.quiet: + w += self.lquiet + self.rquiet + + self.height = self.barHeight + self.width = w + + def draw(self): + oa, oA = ord('a') - 1, ord('A') - 1 + barWidth = self.barWidth + left = self.quiet and self.lquiet or 0 + + for c in self.decomposed: + oc = ord(c) + if c in string.lowercase: + left = left + (oc - oa) * barWidth + elif c in string.uppercase: + w = (oc - oA) * barWidth + self.rect(left, 0, w, self.barHeight) + left += w + self.drawHumanReadable() + +class I2of5(Barcode): + """ + Interleaved 2 of 5 is a numeric-only barcode. It encodes an even + number of digits; if an odd number is given, a 0 is prepended. + + Options that may be passed to constructor: + + value (int, or numeric string. required.): + The value to encode. + + barWidth (float, default .0075): + X-Dimension, or width of the smallest element + Minumum is .0075 inch (7.5 mils). + + ratio (float, default 2.2): + The ratio of wide elements to narrow elements. + Must be between 2.0 and 3.0 (or 2.2 and 3.0 if the + barWidth is greater than 20 mils (.02 inch)) + + gap (float or None, default None): + width of intercharacter gap. None means "use barWidth". + + barHeight (float, see default below): + Height of the symbol. Default is the height of the two + bearer bars (if they exist) plus the greater of .25 inch + or .15 times the symbol's length. + + checksum (bool, default 1): + Whether to compute and include the check digit + + bearers (float, in units of barWidth. default 3.0): + Height of bearer bars (horizontal bars along the top and + bottom of the barcode). Default is 3 x-dimensions. + Set to zero for no bearer bars. (Bearer bars help detect + misscans, so it is suggested to leave them on). + + quiet (bool, default 1): + Whether to include quiet zones in the symbol. + + lquiet (float, see default below): + Quiet zone size to left of code, if quiet is true. + Default is the greater of .25 inch, or .15 times the symbol's + length. + + rquiet (float, defaults as above): + Quiet zone size to right left of code, if quiet is true. + + stop (bool, default 1): + Whether to include start/stop symbols. + + Sources of Information on Interleaved 2 of 5: + + http://www.semiconductor.agilent.com/barcode/sg/Misc/i_25.html + http://www.adams1.com/pub/russadam/i25code.html + + Official Spec, "ANSI/AIM BC2-1995, USS" is available for US$45 from + http://www.aimglobal.org/aimstore/ + """ + + patterns = { + 'start' : 'bsbs', + 'stop' : 'Bsb', + + 'B0' : 'bbBBb', 'S0' : 'ssSSs', + 'B1' : 'BbbbB', 'S1' : 'SsssS', + 'B2' : 'bBbbB', 'S2' : 'sSssS', + 'B3' : 'BBbbb', 'S3' : 'SSsss', + 'B4' : 'bbBbB', 'S4' : 'ssSsS', + 'B5' : 'BbBbb', 'S5' : 'SsSss', + 'B6' : 'bBBbb', 'S6' : 'sSSss', + 'B7' : 'bbbBB', 'S7' : 'sssSS', + 'B8' : 'BbbBb', 'S8' : 'SssSs', + 'B9' : 'bBbBb', 'S9' : 'sSsSs' + } + + barHeight = None + barWidth = inch * 0.0075 + ratio = 2.2 + checksum = 1 + bearers = 3.0 + quiet = 1 + lquiet = None + rquiet = None + stop = 1 + + def __init__(self, value='', **args): + + if type(value) == type(1): + value = str(value) + + for (k, v) in args.items(): + setattr(self, k, v) + + if self.quiet: + if self.lquiet is None: + self.lquiet = min(inch * 0.25, self.barWidth * 10.0) + self.rquiet = min(inch * 0.25, self.barWidth * 10.0) + else: + self.lquiet = self.rquiet = 0.0 + + Barcode.__init__(self, value) + + def validate(self): + vval = "" + self.valid = 1 + for c in string.strip(self.value): + if c not in string.digits: + self.valid = 0 + continue + vval = vval + c + self.validated = vval + return vval + + def encode(self): + s = self.validated + + # make sure result will be a multiple of 2 digits long, + # checksum included + if ((len(self.validated) % 2 == 0) and self.checksum) \ + or ((len(self.validated) % 2 == 1) and not self.checksum): + s = '0' + s + + if self.checksum: + c = 0 + cm = 3 + + for d in s: + c = c + cm * int(d) + if cm == 3: + cm = 1 + else: + cm = 3 + + d = 10 - (int(d) % 10) + s = s + `d` + + self.encoded = s + + def decompose(self): + dval = self.stop and [self.patterns['start']] or [] + a = dval.append + + for i in xrange(0, len(self.encoded), 2): + b = self.patterns['B' + self.encoded[i]] + s = self.patterns['S' + self.encoded[i+1]] + + for i in range(0, len(b)): + a(b[i] + s[i]) + + if self.stop: a(self.patterns['stop']) + self.decomposed = ''.join(dval) + return self.decomposed + +class MSI(Barcode): + """ + MSI is a numeric-only barcode. + + Options that may be passed to constructor: + + value (int, or numeric string. required.): + The value to encode. + + barWidth (float, default .0075): + X-Dimension, or width of the smallest element + + ratio (float, default 2.2): + The ratio of wide elements to narrow elements. + + gap (float or None, default None): + width of intercharacter gap. None means "use barWidth". + + barHeight (float, see default below): + Height of the symbol. Default is the height of the two + bearer bars (if they exist) plus the greater of .25 inch + or .15 times the symbol's length. + + checksum (bool, default 1): + Wether to compute and include the check digit + + bearers (float, in units of barWidth. default 0): + Height of bearer bars (horizontal bars along the top and + bottom of the barcode). Default is 0 (no bearers). + + lquiet (float, see default below): + Quiet zone size to left of code, if quiet is true. + Default is the greater of .25 inch, or 10 barWidths. + + rquiet (float, defaults as above): + Quiet zone size to right left of code, if quiet is true. + + stop (bool, default 1): + Whether to include start/stop symbols. + + Sources of Information on MSI Bar Code: + + http://www.semiconductor.agilent.com/barcode/sg/Misc/msi_code.html + http://www.adams1.com/pub/russadam/plessy.html + """ + + patterns = { + 'start' : 'Bs', 'stop' : 'bSb', + + '0' : 'bSbSbSbS', '1' : 'bSbSbSBs', + '2' : 'bSbSBsbS', '3' : 'bSbSBsBs', + '4' : 'bSBsbSbS', '5' : 'bSBsbSBs', + '6' : 'bSBsBsbS', '7' : 'bSBsBsBs', + '8' : 'BsbSbSbS', '9' : 'BsbSbSBs' + } + + stop = 1 + barHeight = None + barWidth = inch * 0.0075 + ratio = 2.2 + checksum = 1 + bearers = 0.0 + quiet = 1 + lquiet = None + rquiet = None + + def __init__(self, value="", **args): + + if type(value) == type(1): + value = str(value) + + for (k, v) in args.items(): + setattr(self, k, v) + + if self.quiet: + if self.lquiet is None: + self.lquiet = max(inch * 0.25, self.barWidth * 10.0) + self.rquiet = max(inch * 0.25, self.barWidth * 10.0) + else: + self.lquiet = self.rquiet = 0.0 + + Barcode.__init__(self, value) + + def validate(self): + vval = "" + self.valid = 1 + for c in string.strip(self.value): + if c not in string.digits: + self.valid = 0 + continue + vval = vval + c + self.validated = vval + return vval + + def encode(self): + s = self.validated + + if self.checksum: + c = '' + for i in range(1, len(s), 2): + c = c + s[i] + d = str(int(c) * 2) + t = 0 + for c in d: + t = t + int(c) + for i in range(0, len(s), 2): + t = t + int(s[i]) + c = 10 - (t % 10) + + s = s + str(c) + + self.encoded = s + + def decompose(self): + dval = self.stop and [self.patterns['start']] or [] + dval += [self.patterns[c] for c in self.encoded] + if self.stop: dval.append(self.patterns['stop']) + self.decomposed = ''.join(dval) + return self.decomposed + +class Codabar(Barcode): + """ + Codabar is a numeric plus some puntuation ("-$:/.+") barcode + with four start/stop characters (A, B, C, and D). + + Options that may be passed to constructor: + + value (string. required.): + The value to encode. + + barWidth (float, default .0065): + X-Dimension, or width of the smallest element + minimum is 6.5 mils (.0065 inch) + + ratio (float, default 2.0): + The ratio of wide elements to narrow elements. + + gap (float or None, default None): + width of intercharacter gap. None means "use barWidth". + + barHeight (float, see default below): + Height of the symbol. Default is the height of the two + bearer bars (if they exist) plus the greater of .25 inch + or .15 times the symbol's length. + + checksum (bool, default 0): + Whether to compute and include the check digit + + bearers (float, in units of barWidth. default 0): + Height of bearer bars (horizontal bars along the top and + bottom of the barcode). Default is 0 (no bearers). + + quiet (bool, default 1): + Whether to include quiet zones in the symbol. + + stop (bool, default 1): + Whether to include start/stop symbols. + + lquiet (float, see default below): + Quiet zone size to left of code, if quiet is true. + Default is the greater of .25 inch, or 10 barWidth + + rquiet (float, defaults as above): + Quiet zone size to right left of code, if quiet is true. + + Sources of Information on Codabar + + http://www.semiconductor.agilent.com/barcode/sg/Misc/codabar.html + http://www.barcodeman.com/codabar.html + + Official Spec, "ANSI/AIM BC3-1995, USS" is available for US$45 from + http://www.aimglobal.org/aimstore/ + """ + + patterns = { + '0': 'bsbsbSB', '1': 'bsbsBSb', '2': 'bsbSbsB', + '3': 'BSbsbsb', '4': 'bsBsbSb', '5': 'BsbsbSb', + '6': 'bSbsbsB', '7': 'bSbsBsb', '8': 'bSBsbsb', + '9': 'BsbSbsb', '-': 'bsbSBsb', '$': 'bsBSbsb', + ':': 'BsbsBsB', '/': 'BsBsbsB', '.': 'BsBsBsb', + '+': 'bsBsBsB', 'A': 'bsBSbSb', 'B': 'bSbSbsB', + 'C': 'bsbSbSB', 'D': 'bsbSBSb' + } + + values = { + '0' : 0, '1' : 1, '2' : 2, '3' : 3, '4' : 4, + '5' : 5, '6' : 6, '7' : 7, '8' : 8, '9' : 9, + '-' : 10, '$' : 11, ':' : 12, '/' : 13, '.' : 14, + '+' : 15, 'A' : 16, 'B' : 17, 'C' : 18, 'D' : 19 + } + + chars = string.digits + "-$:/.+" + + stop = 1 + barHeight = None + barWidth = inch * 0.0065 + ratio = 2.0 # XXX ? + checksum = 0 + bearers = 0.0 + quiet = 1 + lquiet = None + rquiet = None + + def __init__(self, value='', **args): + if type(value) == type(1): + value = str(value) + + for (k, v) in args.items(): + setattr(self, k, v) + + if self.quiet: + if self.lquiet is None: + self.lquiet = min(inch * 0.25, self.barWidth * 10.0) + self.rquiet = min(inch * 0.25, self.barWidth * 10.0) + else: + self.lquiet = self.rquiet = 0.0 + + Barcode.__init__(self, value) + + def validate(self): + vval = "" + self.valid = 1 + s = string.strip(self.value) + for i in range(0, len(s)): + c = s[i] + if c not in self.chars: + if ((i != 0) and (i != len(s) - 1)) or (c not in 'ABCD'): + self.Valid = 0 + continue + vval = vval + c + + if self.stop: + if vval[0] not in 'ABCD': + vval = 'A' + vval + if vval[-1] not in 'ABCD': + vval = vval + vval[0] + + self.validated = vval + return vval + + def encode(self): + s = self.validated + + if self.checksum: + v = sum([self.values[c] for c in s]) + s += self.chars[v % 16] + + self.encoded = s + + def decompose(self): + dval = ''.join([self.patterns[c]+'i' for c in self.encoded]) + self.decomposed = dval[:-1] + return self.decomposed + +class Code11(Barcode): + """ + Code 11 is an almost-numeric barcode. It encodes the digits 0-9 plus + dash ("-"). 11 characters total, hence the name. + + value (int or string. required.): + The value to encode. + + barWidth (float, default .0075): + X-Dimension, or width of the smallest element + + ratio (float, default 2.2): + The ratio of wide elements to narrow elements. + + gap (float or None, default None): + width of intercharacter gap. None means "use barWidth". + + barHeight (float, see default below): + Height of the symbol. Default is the height of the two + bearer bars (if they exist) plus the greater of .25 inch + or .15 times the symbol's length. + + checksum (0 none, 1 1-digit, 2 2-digit, -1 auto, default -1): + How many checksum digits to include. -1 ("auto") means + 1 if the number of digits is 10 or less, else 2. + + bearers (float, in units of barWidth. default 0): + Height of bearer bars (horizontal bars along the top and + bottom of the barcode). Default is 0 (no bearers). + + quiet (bool, default 1): + Wether to include quiet zones in the symbol. + + lquiet (float, see default below): + Quiet zone size to left of code, if quiet is true. + Default is the greater of .25 inch, or 10 barWidth + + rquiet (float, defaults as above): + Quiet zone size to right left of code, if quiet is true. + + Sources of Information on Code 11: + + http://www.cwi.nl/people/dik/english/codes/barcodes.html + """ + + chars = string.digits + '-' + + patterns = { + '0' : 'bsbsB', '1' : 'BsbsB', '2' : 'bSbsB', + '3' : 'BSbsb', '4' : 'bsBsB', '5' : 'BsBsb', + '6' : 'bSBsb', '7' : 'bsbSB', '8' : 'BsbSb', + '9' : 'Bsbsb', '-' : 'bsBsb', 'S' : 'bsBSb' # Start/Stop + } + + values = { + '0' : 0, '1' : 1, '2' : 2, '3' : 3, '4' : 4, + '5' : 5, '6' : 6, '7' : 7, '8' : 8, '9' : 9, + '-' : 10, + } + + stop = 1 + barHeight = None + barWidth = inch * 0.0075 + ratio = 2.2 # XXX ? + checksum = -1 # Auto + bearers = 0.0 + quiet = 1 + lquiet = None + rquiet = None + def __init__(self, value='', **args): + if type(value) == type(1): + value = str(value) + + for (k, v) in args.items(): + setattr(self, k, v) + + if self.quiet: + if self.lquiet is None: + self.lquiet = min(inch * 0.25, self.barWidth * 10.0) + self.rquiet = min(inch * 0.25, self.barWidth * 10.0) + else: + self.lquiet = self.rquiet = 0.0 + + Barcode.__init__(self, value) + + def validate(self): + vval = "" + self.valid = 1 + s = string.strip(self.value) + for i in range(0, len(s)): + c = s[i] + if c not in self.chars: + self.Valid = 0 + continue + vval = vval + c + + self.validated = vval + return vval + + def encode(self): + s = self.validated + + if self.checksum == -1: + if len(s) <= 10: + self.checksum = 1 + else: + self.checksum = 2 + + if self.checksum > 0: + # compute first checksum + i = 0; v = 1; c = 0 + while i < len(s): + c = c + v * string.index(self.chars, s[-(i+1)]) + i = i + 1; v = v + 1 + if v > 10: + v = 1 + s = s + self.chars[c % 11] + + if self.checksum > 1: + # compute second checksum + i = 0; v = 1; c = 0 + while i < len(s): + c = c + v * string.index(self.chars, s[-(i+1)]) + i = i + 1; v = v + 1 + if v > 9: + v = 1 + s = s + self.chars[c % 10] + + self.encoded = self.stop and ('S' + s + 'S') or s + + def decompose(self): + dval = [self.patterns[c]+'i' for c in self.encoded] + self.decomposed = ''.join(dval[:-1]) + return self.decomposed + + def _humanText(self): + return self.stop and self.encoded[1:-1] or self.encoded diff --git a/bin/reportlab/graphics/barcode/eanbc.py b/bin/reportlab/graphics/barcode/eanbc.py new file mode 100644 index 00000000000..f9fbc73b61c --- /dev/null +++ b/bin/reportlab/graphics/barcode/eanbc.py @@ -0,0 +1,339 @@ +__all__=( + 'Ean13BarcodeWidget','isEanString', + ) +from reportlab.graphics.shapes import Group, String, Rect +from reportlab.lib import colors +from reportlab.pdfbase.pdfmetrics import stringWidth +from reportlab.lib.validators import isNumber, isColor, isString, Validator, isBoolean +from reportlab.lib.attrmap import * +from reportlab.graphics.charts.areas import PlotArea +from reportlab.lib.units import mm + +#work out a list of manufacturer codes.... +_eanNumberSystems = [ + ('00-13', 'USA & Canada'), + ('20-29', 'In-Store Functions'), + ('30-37', 'France'), + ('40-44', 'Germany'), + ('45', 'Japan (also 49)'), + ('46', 'Russian Federation'), + ('471', 'Taiwan'), + ('474', 'Estonia'), + ('475', 'Latvia'), + ('477', 'Lithuania'), + ('479', 'Sri Lanka'), + ('480', 'Philippines'), + ('482', 'Ukraine'), + ('484', 'Moldova'), + ('485', 'Armenia'), + ('486', 'Georgia'), + ('487', 'Kazakhstan'), + ('489', 'Hong Kong'), + ('49', 'Japan (JAN-13)'), + ('50', 'United Kingdom'), + ('520', 'Greece'), + ('528', 'Lebanon'), + ('529', 'Cyprus'), + ('531', 'Macedonia'), + ('535', 'Malta'), + ('539', 'Ireland'), + ('54', 'Belgium & Luxembourg'), + ('560', 'Portugal'), + ('569', 'Iceland'), + ('57', 'Denmark'), + ('590', 'Poland'), + ('594', 'Romania'), + ('599', 'Hungary'), + ('600-601', 'South Africa'), + ('609', 'Mauritius'), + ('611', 'Morocco'), + ('613', 'Algeria'), + ('619', 'Tunisia'), + ('622', 'Egypt'), + ('625', 'Jordan'), + ('626', 'Iran'), + ('64', 'Finland'), + ('690-692', 'China'), + ('70', 'Norway'), + ('729', 'Israel'), + ('73', 'Sweden'), + ('740', 'Guatemala'), + ('741', 'El Salvador'), + ('742', 'Honduras'), + ('743', 'Nicaragua'), + ('744', 'Costa Rica'), + ('746', 'Dominican Republic'), + ('750', 'Mexico'), + ('759', 'Venezuela'), + ('76', 'Switzerland'), + ('770', 'Colombia'), + ('773', 'Uruguay'), + ('775', 'Peru'), + ('777', 'Bolivia'), + ('779', 'Argentina'), + ('780', 'Chile'), + ('784', 'Paraguay'), + ('785', 'Peru'), + ('786', 'Ecuador'), + ('789', 'Brazil'), + ('80-83', 'Italy'), + ('84', 'Spain'), + ('850', 'Cuba'), + ('858', 'Slovakia'), + ('859', 'Czech Republic'), + ('860', 'Yugloslavia'), + ('869', 'Turkey'), + ('87', 'Netherlands'), + ('880', 'South Korea'), + ('885', 'Thailand'), + ('888', 'Singapore'), + ('890', 'India'), + ('893', 'Vietnam'), + ('899', 'Indonesia'), + ('90-91', 'Austria'), + ('93', 'Australia'), + ('94', 'New Zealand'), + ('955', 'Malaysia'), + ('977', 'International Standard Serial Number for Periodicals (ISSN)'), + ('978', 'International Standard Book Numbering (ISBN)'), + ('979', 'International Standard Music Number (ISMN)'), + ('980', 'Refund receipts'), + ('981-982', 'Common Currency Coupons'), + ('99', 'Coupons') + ] + +manufacturerCodes = {} +for (k, v) in _eanNumberSystems: + words = k.split('-') + if len(words)==2: + fromCode = int(words[0]) + toCode = int(words[1]) + for code in range(fromCode, toCode+1): + manufacturerCodes[code] = v + else: + manufacturerCodes[int(k)] = v + +class isEan13String(Validator): + def test(self,x): + return type(x) is str and len(x)<=12 and len([c for c in x if c in "0123456789"])==12 +isEan13String = isEan13String() + +class Ean13BarcodeWidget(PlotArea): + codeName = "EAN13" + _attrMap = AttrMap(BASE=PlotArea, + value = AttrMapValue(isEan13String, desc='the number'), + fontName = AttrMapValue(isString, desc='fontName'), + fontSize = AttrMapValue(isNumber, desc='font size'), + x = AttrMapValue(isNumber, desc='x-coord'), + y = AttrMapValue(isNumber, desc='y-coord'), + barFillColor = AttrMapValue(isColor, desc='bar color'), + barHeight = AttrMapValue(isNumber, desc='Height of bars.'), + barWidth = AttrMapValue(isNumber, desc='Width of bars.'), + barStrokeWidth = AttrMapValue(isNumber, desc='Width of bar borders.'), + barStrokeColor = AttrMapValue(isColor, desc='Color of bar borders.'), + textColor = AttrMapValue(isColor, desc='human readable text color'), + humanReadable = AttrMapValue(isBoolean, desc='if human readable'), + quiet = AttrMapValue(isBoolean, desc='if quiet zone to be used'), + lquiet = AttrMapValue(isBoolean, desc='left quiet zone length'), + rquiet = AttrMapValue(isBoolean, desc='right quiet zone length'), + ) + _digits=12 + _start_right = 7 #for ean-13 left = [0:7] right=[7:13] + _nbars = 113 + barHeight = 25.93*mm #millimeters + barWidth = (37.29/_nbars)*mm + humanReadable = 1 + _0csw = 1 + _1csw = 3 + + #Left Hand Digits. + _left = ( ("0001101", "0011001", "0010011", "0111101", + "0100011", "0110001", "0101111", "0111011", + "0110111", "0001011", + ), #odd left hand digits + ("0100111", "0110011", "0011011", "0100001", + "0011101", "0111001", "0000101", "0010001", + "0001001", "0010111"), #even left hand digits + ) + + _right = ("1110010", "1100110", "1101100", "1000010", + "1011100", "1001110", "1010000", "1000100", + "1001000", "1110100") + + quiet = 1 + rquiet = lquiet = None + _tail = "101" + _sep = "01010" + + _lhconvert={ + "0": (0,0,0,0,0,0), + "1": (0,0,1,0,1,1), + "2": (0,0,1,1,0,1), + "3": (0,0,1,1,1,0), + "4": (0,1,0,0,1,1), + "5": (0,1,1,0,0,1), + "6": (0,1,1,1,0,0), + "7": (0,1,0,1,0,1), + "8": (0,1,0,1,1,0), + "9": (0,1,1,0,1,0) + } + fontSize = 8 #millimeters + fontName = 'Helvetica' + textColor = barFillColor = barStrokeColor = colors.black + barStrokeWidth = 0 + x = 0 + y = 0 + def __init__(self,value='123456789012',**kw): + self.value=max(self._digits-len(value),0)*'0'+value[:self._digits] + for k, v in kw.iteritems(): + setattr(self, k, v) + + width = property(lambda self: self.barWidth*(self._nbars-18+self._calc_quiet(self.lquiet)+self._calc_quiet(self.rquiet))) + + def wrap(self,aW,aH): + return self.width,self.barHeight + + def _encode_left(self,s,a): + cp = self._lhconvert[s[0]] #convert the left hand numbers + _left = self._left + z = ord('0') + for i,c in enumerate(s[1:self._start_right]): + a(_left[cp[i]][ord(c)-z]) + + def _short_bar(self,i): + i += 9 - self._lquiet + return self.humanReadable and ((120: v += 1 + else: + v = 0 + return v + + def draw(self): + g = Group() + gAdd = g.add + barWidth = self.barWidth + width = self.width + barHeight = self.barHeight + x = self.x + y = self.y + gAdd(Rect(x,y,width,barHeight,fillColor=None,strokeColor=None,strokeWidth=0)) + s = self.value+self._checkdigit(self.value) + self._lquiet = lquiet = self._calc_quiet(self.lquiet) + rquiet = self._calc_quiet(self.rquiet) + b = [lquiet*'0',self._tail] #the signal string + a = b.append + self._encode_left(s,a) + a(self._sep) + + z = ord('0') + _right = self._right + for c in s[self._start_right:]: + a(_right[ord(c)-z]) + a(self._tail) + a(rquiet*'0') + + fontSize = self.fontSize + barFillColor = self.barFillColor + barStrokeWidth = self.barStrokeWidth + + fth = fontSize*1.2 + b = ''.join(b) + + lrect = None + for i,c in enumerate(b): + if c=="1": + dh = self._short_bar(i) and fth or 0 + yh = y+dh + if lrect and lrect.y==yh: + lrect.width += barWidth + else: + lrect = Rect(x,yh,barWidth,barHeight-dh,fillColor=barFillColor,strokeWidth=barStrokeWidth,strokeColor=barFillColor) + gAdd(lrect) + else: + lrect = None + x += barWidth + + if self.humanReadable: self._add_human_readable(s,gAdd) + return g + + def _add_human_readable(self,s,gAdd): + barWidth = self.barWidth + fontSize = self.fontSize + textColor = self.textColor + fontName = self.fontName + fth = fontSize*1.2 + # draw the num below the line. + c = s[0] + w = stringWidth(c,fontName,fontSize) + x = self.x+barWidth*(self._lquiet-8) + y = self.y + 0.2*fth + + gAdd(String(x,y,c,fontName=fontName,fontSize=fontSize,fillColor=textColor)) + x = self.x + (33-9+self._lquiet)*barWidth + + c = s[1:7] + gAdd(String(x,y,c,fontName=fontName,fontSize=fontSize,fillColor=textColor,textAnchor='middle')) + + x += 47*barWidth + c = s[7:] + gAdd(String(x,y,c,fontName=fontName,fontSize=fontSize,fillColor=textColor,textAnchor='middle')) + + @classmethod + def _checkdigit(cls,num): + z = ord('0') + iSum = cls._0csw*sum([(ord(x)-z) for x in num[::2]]) \ + + cls._1csw*sum([(ord(x)-z) for x in num[1::2]]) + return chr(z+((10-(iSum%10))%10)) + +class isEan8String(Validator): + def test(self,x): + return type(x) is str and len(x)<=7 and len([c for c in x if c in "0123456789"])==7 +isEan8String = isEan8String() + +class Ean8BarcodeWidget(Ean13BarcodeWidget): + codeName = "EAN8" + _attrMap = AttrMap(BASE=Ean13BarcodeWidget, + value = AttrMapValue(isEan8String, desc='the number'), + ) + _start_right = 4 #for ean-13 left = [0:7] right=[7:13] + _nbars = 85 + _digits=7 + _0csw = 3 + _1csw = 1 + + def _encode_left(self,s,a): + cp = self._lhconvert[s[0]] #convert the left hand numbers + _left = self._left[0] + z = ord('0') + for i,c in enumerate(s[0:self._start_right]): + a(_left[ord(c)-z]) + + def _short_bar(self,i): + i += 9 - self._lquiet + return self.humanReadable and ((12 +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. All advertising materials mentioning features or use of this software +# must display the following acknowledgement: +# This product includes software developed by Tyler C. Sarna. +# 4. Neither the name of the author nor the names of contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +from reportlab.lib.units import inch +from common import Barcode +import string + +# . 3 T Tracker +# , 2 D Descender +# ' 1 A Ascender +# | 0 H Ascender/Descender + +_rm_patterns = { + "0" : "--||", "1" : "-',|", "2" : "-'|,", "3" : "'-,|", + "4" : "'-|,", "5" : "'',,", "6" : "-,'|", "7" : "-|-|", + "8" : "-|',", "9" : "',-|", "A" : "',',", "B" : "'|-,", + "C" : "-,|'", "D" : "-|,'", "E" : "-||-", "F" : "',,'", + "G" : "',|-", "H" : "'|,-", "I" : ",-'|", "J" : ",'-|", + "K" : ",'',", "L" : "|--|", "M" : "|-',", "N" : "|'-,", + "O" : ",-|'", "P" : ",','", "Q" : ",'|-", "R" : "|-,'", + "S" : "|-|-", "T" : "|',-", "U" : ",,''", "V" : ",|-'", + "W" : ",|'-", "X" : "|,-'", "Y" : "|,'-", "Z" : "||--", + + # start, stop + "(" : "'-,'", ")" : "'|,|" +} + +_ozN_patterns = { + "0" : "||", "1" : "|'", "2" : "|,", "3" : "'|", "4" : "''", + "5" : "',", "6" : ",|", "7" : ",'", "8" : ",,", "9" : ".|" +} + +_ozC_patterns = { + "A" : "|||", "B" : "||'", "C" : "||,", "D" : "|'|", + "E" : "|''", "F" : "|',", "G" : "|,|", "H" : "|,'", + "I" : "|,,", "J" : "'||", "K" : "'|'", "L" : "'|,", + "M" : "''|", "N" : "'''", "O" : "'',", "P" : "',|", + "Q" : "','", "R" : "',,", "S" : ",||", "T" : ",|'", + "U" : ",|,", "V" : ",'|", "W" : ",''", "X" : ",',", + "Y" : ",,|", "Z" : ",,'", "a" : "|,.", "b" : "|.|", + "c" : "|.'", "d" : "|.,", "e" : "|..", "f" : "'|.", + "g" : "''.", "h" : "',.", "i" : "'.|", "j" : "'.'", + "k" : "'.,", "l" : "'..", "m" : ",|.", "n" : ",'.", + "o" : ",,.", "p" : ",.|", "q" : ",.'", "r" : ",.,", + "s" : ",..", "t" : ".|.", "u" : ".'.", "v" : ".,.", + "w" : "..|", "x" : "..'", "y" : "..,", "z" : "...", + "0" : ",,,", "1" : ".||", "2" : ".|'", "3" : ".|,", + "4" : ".'|", "5" : ".''", "6" : ".',", "7" : ".,|", + "8" : ".,'", "9" : ".,,", " " : "||.", "#" : "|'.", +} + +#http://www.auspost.com.au/futurepost/ diff --git a/bin/reportlab/graphics/barcode/test.py b/bin/reportlab/graphics/barcode/test.py new file mode 100644 index 00000000000..3342947672e --- /dev/null +++ b/bin/reportlab/graphics/barcode/test.py @@ -0,0 +1,185 @@ +#!/usr/pkg/bin/python + +import os, sys, time + +from reportlab.graphics.barcode.common import * +from reportlab.graphics.barcode.code39 import * +from reportlab.graphics.barcode.code93 import * +from reportlab.graphics.barcode.code128 import * +from reportlab.graphics.barcode.usps import * + + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation +from reportlab.platypus import Spacer, SimpleDocTemplate, Table, TableStyle, Preformatted, PageBreak +from reportlab.lib.units import inch, cm +from reportlab.lib import colors + +from reportlab.pdfgen.canvas import Canvas +from reportlab.lib.styles import getSampleStyleSheet +from reportlab.platypus.paragraph import Paragraph +from reportlab.platypus.frames import Frame +from reportlab.platypus.flowables import XBox, KeepTogether +from reportlab.graphics.shapes import Drawing + +from reportlab.graphics.barcode import getCodes, getCodeNames, createBarcodeDrawing +def run(): + styles = getSampleStyleSheet() + styleN = styles['Normal'] + styleH = styles['Heading1'] + story = [] + + #for codeNames in code + story.append(Paragraph('I2of5', styleN)) + story.append(I2of5(1234, barWidth = inch*0.02, checksum=0)) + story.append(Paragraph('MSI', styleN)) + story.append(MSI(1234)) + story.append(Paragraph('Codabar', styleN)) + story.append(Codabar("A012345B", barWidth = inch*0.02)) + story.append(Paragraph('Code 11', styleN)) + story.append(Code11("01234545634563")) + story.append(Paragraph('Code 39', styleN)) + story.append(Standard39("A012345B%R")) + story.append(Paragraph('Extended Code 39', styleN)) + story.append(Extended39("A012345B}")) + story.append(Paragraph('Code93', styleN)) + story.append(Standard93("CODE 93")) + story.append(Paragraph('Extended Code93', styleN)) + story.append(Extended93("L@@K! Code 93 :-)")) #, barWidth=0.005 * inch)) + story.append(Paragraph('Code 128', styleN)) + c=Code128("AB-12345678") #, barWidth=0.005 * inch) + #print 'WIDTH =', (c.width / inch), 'barWidth =', (c.barWidth / inch) + #print 'LQ =', (c.lquiet / inch), 'RQ =', (c.rquiet / inch) + story.append(c) + story.append(Paragraph('USPS FIM', styleN)) + story.append(FIM("A")) + story.append(Paragraph('USPS POSTNET', styleN)) + story.append(POSTNET('78247-1043')) + + from reportlab.graphics.barcode import createBarcodeDrawing + story.append(Paragraph('EAN13', styleN)) + bcd = createBarcodeDrawing('EAN13', value='123456789012') + story.append(bcd) + story.append(Paragraph('EAN8', styleN)) + bcd = createBarcodeDrawing('EAN8', value='1234567') + story.append(bcd) + + story.append(Paragraph('Label Size', styleN)) + story.append(XBox((2.0 + 5.0/8.0)*inch, 1 * inch, '1x2-5/8"')) + story.append(Paragraph('Label Size', styleN)) + story.append(XBox((1.75)*inch, .5 * inch, '1/2x1-3/4"')) + c = Canvas('out.pdf') + f = Frame(inch, inch, 6*inch, 9*inch, showBoundary=1) + f.addFromList(story, c) + c.save() + print 'saved out.pdf' + +def fullTest(fileName="test_full.pdf"): + """Creates large-ish test document with a variety of parameters""" + + story = [] + + styles = getSampleStyleSheet() + styleN = styles['Normal'] + styleH = styles['Heading1'] + styleH2 = styles['Heading2'] + story = [] + + story.append(Paragraph('ReportLab Barcode Test Suite - full output', styleH)) + story.append(Paragraph('Generated on %s' % time.ctime(time.time()), styleN)) + + story.append(Paragraph('', styleN)) + story.append(Paragraph('Repository information for this build:', styleN)) + #see if we can figure out where it was built, if we're running in source + if os.path.split(os.getcwd())[-1] == 'barcode' and os.path.isdir('.svn'): + #runnning in a filesystem svn copy + infoLines = os.popen('svn info').read() + story.append(Preformatted(infoLines, styles["Code"])) + + story.append(Paragraph('About this document', styleH2)) + story.append(Paragraph('History and Status', styleH2)) + + story.append(Paragraph(""" + This is the test suite and docoumentation for the ReportLab open source barcode API, + being re-released as part of the forthcoming ReportLab 2.0 release. + """, styleN)) + + story.append(Paragraph(""" + Several years ago Ty Sarna contributed a barcode module to the ReportLab community. + Several of the codes were used by him in hiw work and to the best of our knowledge + this was correct. These were written as flowable objects and were available in PDFs, + but not in our graphics framework. However, we had no knowledge of barcodes ourselves + and did not advertise or extend the package. + """, styleN)) + + story.append(Paragraph(""" + We "wrapped" the barcodes to be usable within our graphics framework; they are now available + as Drawing objects which can be rendered to EPS files or bitmaps. For the last 2 years this + has been available in our Diagra and Report Markup Language products. However, we did not + charge separately and use was on an "as is" basis. + """, styleN)) + + story.append(Paragraph(""" + A major licensee of our technology has kindly agreed to part-fund proper productisation + of this code on an open source basis in Q1 2006. This has involved addition of EAN codes + as well as a proper testing program. Henceforth we intend to publicise the code more widely, + gather feedback, accept contributions of code and treat it as "supported". + """, styleN)) + + story.append(Paragraph(""" + This involved making available both downloads and testing resources. This PDF document + is the output of the current test suite. It contains codes you can scan (if you use a nice sharp + laser printer!), and will be extended over coming weeks to include usage examples and notes on + each barcode and how widely tested they are. This is being done through documentation strings in + the barcode objects themselves so should always be up to date. + """, styleN)) + + story.append(Paragraph('Usage examples', styleH2)) + story.append(Paragraph(""" + To be completed + """, styleN)) + + story.append(Paragraph('The codes', styleH2)) + story.append(Paragraph(""" + Below we show a scannable code from each barcode, with and without human-readable text. + These are magnified about 2x from the natural size done by the original author to aid + inspection. This will be expanded to include several test cases per code, and to add + explanations of checksums. Be aware that (a) if you enter numeric codes which are too + short they may be prefixed for you (e.g. "123" for an 8-digit code becomes "00000123"), + and that the scanned results and readable text will generally include extra checksums + at the end. + """, styleN)) + + codeNames = getCodeNames() + from reportlab.lib.utils import flatten + width = [float(x[8:]) for x in sys.argv if x.startswith('--width=')] + height = [float(x[9:]) for x in sys.argv if x.startswith('--height=')] + isoScale = [int(x[11:]) for x in sys.argv if x.startswith('--isoscale=')] + options = {} + if width: options['width'] = width[0] + if height: options['height'] = height[0] + if isoScale: options['isoScale'] = isoScale[0] + scales = [x[8:].split(',') for x in sys.argv if x.startswith('--scale=')] + scales = map(float,scales and flatten(scales) or [1]) + scales = map(float,scales and flatten(scales) or [1]) + for scale in scales: + story.append(PageBreak()) + story.append(Paragraph('Scale = %.1f'%scale, styleH2)) + story.append(Spacer(36, 12)) + for codeName in codeNames: + s = [Paragraph('Code: ' + codeName, styleH2)] + for hr in (0,1): + s.append(Spacer(36, 12)) + dr = createBarcodeDrawing(codeName, humanReadable=hr,**options) + dr.renderScale = scale + s.append(dr) + s.append(Spacer(36, 12)) + s.append(Paragraph('Barcode should say: ' + dr._bc.value, styleN)) + story.append(KeepTogether(s)) + + SimpleDocTemplate(fileName).build(story) + print 'created', fileName + +if __name__=='__main__': + run() + fullTest() diff --git a/bin/reportlab/graphics/barcode/usps.py b/bin/reportlab/graphics/barcode/usps.py new file mode 100644 index 00000000000..999dd9ca340 --- /dev/null +++ b/bin/reportlab/graphics/barcode/usps.py @@ -0,0 +1,228 @@ +# +# Copyright (c) 1996-2000 Tyler C. Sarna +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. All advertising materials mentioning features or use of this software +# must display the following acknowledgement: +# This product includes software developed by Tyler C. Sarna. +# 4. Neither the name of the author nor the names of contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +from reportlab.lib.units import inch +from common import Barcode +import string + +_fim_patterns = { + 'A' : "|| | ||", + 'B' : "| || || |", + 'C' : "|| | | ||", + 'D' : "||| | |||", + # XXX There is an E. + # The below has been seen, but dunno if it is E or not: + # 'E' : '|||| ||||' +} + +_postnet_patterns = { + '1' : "...||", '2' : "..|.|", '3' : "..||.", '4' : ".|..|", + '5' : ".|.|.", '6' : ".||..", '7' : "|...|", '8' : "|..|.", + '9' : "|.|..", '0' : "||...", 'S' : "|", +} + +class FIM(Barcode): + """" + FIM (Facing ID Marks) encode only one letter. + There are currently four defined: + + A Courtesy reply mail with pre-printed POSTNET + B Business reply mail without pre-printed POSTNET + C Business reply mail with pre-printed POSTNET + D OCR Readable mail without pre-printed POSTNET + + Options that may be passed to constructor: + + value (single character string from the set A - D. required.): + The value to encode. + + quiet (bool, default 0): + Whether to include quiet zones in the symbol. + + The following may also be passed, but doing so will generate nonstandard + symbols which should not be used. This is mainly documented here to + show the defaults: + + barHeight (float, default 5/8 inch): + Height of the code. This might legitimately be overriden to make + a taller symbol that will 'bleed' off the edge of the paper, + leaving 5/8 inch remaining. + + lquiet (float, default 1/4 inch): + Quiet zone size to left of code, if quiet is true. + Default is the greater of .25 inch, or .15 times the symbol's + length. + + rquiet (float, default 15/32 inch): + Quiet zone size to right left of code, if quiet is true. + + Sources of information on FIM: + + USPS Publication 25, A Guide to Business Mail Preparation + http://new.usps.com/cpim/ftp/pubs/pub25.pdf + """ + barWidth = inch * (1.0/32.0) + spaceWidth = inch * (1.0/16.0) + barHeight = inch * (5.0/8.0) + rquiet = inch * (0.25) + lquiet = inch * (15.0/32.0) + quiet = 0 + def __init__(self, value='', **args): + for (k, v) in args.items(): + setattr(self, k, v) + + Barcode.__init__(self, value) + + def validate(self): + self.valid = 1 + self.validated = '' + for c in self.value: + if c in string.whitespace: + continue + elif c in "abcdABCD": + self.validated = self.validated + string.upper(c) + else: + self.valid = 0 + + if len(self.validated) != 1: + raise ValueError, "Input must be exactly one character" + + return self.validated + + def decompose(self): + self.decomposed = '' + for c in self.encoded: + self.decomposed = self.decomposed + _fim_patterns[c] + + return self.decomposed + + def computeSize(self): + self.width = (len(self.decomposed) - 1) * self.spaceWidth + self.barWidth + if self.quiet: + self.width += self.lquiet + self.rquiet + self.height = self.barHeight + + def draw(self): + left = self.quiet and self.lquiet or 0 + for c in self.decomposed: + if c == '|': + self.rect(left, 0.0, self.barWidth, self.barHeight) + left += self.spaceWidth + self.drawHumanReadable() + + def _humanText(self): + return self.value + +class POSTNET(Barcode): + """" + POSTNET is used in the US to encode "zip codes" (postal codes) on + mail. It can encode 5, 9, or 11 digit codes. I've read that it's + pointless to do 5 digits, since USPS will just have to re-print + them with 9 or 11 digits. + + Sources of information on POSTNET: + + USPS Publication 25, A Guide to Business Mail Preparation + http://new.usps.com/cpim/ftp/pubs/pub25.pdf + """ + quiet = 0 + shortHeight = inch * 0.050 + barHeight = inch * 0.125 + barWidth = inch * 0.018 + spaceWidth = inch * 0.0275 + def __init__(self, value='', **args): + + for (k, v) in args.items(): + setattr(self, k, v) + + Barcode.__init__(self, value) + + def validate(self): + self.validated = '' + self.valid = 1 + count = 0 + for c in self.value: + if c in (string.whitespace + '-'): + pass + elif c in string.digits: + count = count + 1 + if count == 6: + self.validated = self.validated + '-' + self.validated = self.validated + c + else: + self.valid = 0 + + if len(self.validated) not in [5, 10, 12]: + self.valid = 0 + + return self.validated + + def encode(self): + self.encoded = "S" + check = 0 + for c in self.validated: + if c in string.digits: + self.encoded = self.encoded + c + check = check + string.atoi(c) + elif c == '-': + pass + else: + raise ValueError, "Invalid character in input" + check = (10 - (check % 10)) % 10 + self.encoded = self.encoded + `check` + 'S' + return self.encoded + + def decompose(self): + self.decomposed = '' + for c in self.encoded: + self.decomposed = self.decomposed + _postnet_patterns[c] + return self.decomposed + + def computeSize(self): + self.width = len(self.decomposed) * self.barWidth + (len(self.decomposed) - 1) * self.spaceWidth + self.height = self.barHeight + + def draw(self): + sdown = self.barHeight - self.shortHeight + left = 0 + + for c in self.decomposed: + if c == '.': + h = self.shortHeight + else: + h = self.barHeight + self.rect(left, 0.0, self.barWidth, h) + left = left + self.barWidth + self.spaceWidth + self.drawHumanReadable() + + def _humanText(self): + return self.encoded[1:-1] diff --git a/bin/reportlab/graphics/barcode/widgets.py b/bin/reportlab/graphics/barcode/widgets.py new file mode 100644 index 00000000000..b350d513157 --- /dev/null +++ b/bin/reportlab/graphics/barcode/widgets.py @@ -0,0 +1,304 @@ +#copyright ReportLab Europe Limited. 2000-2006 +#see license.txt for license details +__version__=''' $Id: widgets.py 2851 2006-05-08 14:34:45Z rgbecker $ ''' +__all__= ( + 'BarcodeI2of5', + 'BarcodeCode128', + 'BarcodeStandard93', + 'BarcodeExtended93', + 'BarcodeStandard39', + 'BarcodeExtended39', + 'BarcodeMSI', + 'BarcodeCodabar', + 'BarcodeCode11', + 'BarcodeFIM', + 'BarcodePOSTNET', + ) + +from reportlab.lib.validators import isInt, isNumber, isColor, isString, isColorOrNone, OneOf, isBoolean, EitherOr, isNumberOrNone +from reportlab.lib.attrmap import AttrMap, AttrMapValue +from reportlab.lib.colors import black +from reportlab.graphics.shapes import Line, Rect, Group, NotImplementedError, String +from reportlab.graphics.charts.areas import PlotArea + +''' +#snippet + +#first make your Drawing +from reportlab.graphics.shapes import Drawing +d= Drawing(100,50) + +#create and set up the widget +from reportlab.graphics.barcode.widgets import BarcodeStandard93 +bc = BarcodeStandard93() +bc.value = 'RGB-123456' + +#add to the drawing and save +d.add(bc) +# d.save(formats=['gif','pict'],fnRoot='bc_sample') +''' + +class _BarcodeWidget(PlotArea): + _attrMap = AttrMap(BASE=PlotArea, + barStrokeColor = AttrMapValue(isColorOrNone, desc='Color of bar borders.'), + barFillColor = AttrMapValue(isColorOrNone, desc='Color of bar interior areas.'), + barStrokeWidth = AttrMapValue(isNumber, desc='Width of bar borders.'), + value = AttrMapValue(EitherOr((isString,isNumber)), desc='Value.'), + textColor = AttrMapValue(isColorOrNone, desc='Color of human readable text.'), + valid = AttrMapValue(isBoolean), + validated = AttrMapValue(isString,desc="validated form of input"), + encoded = AttrMapValue(None,desc="encoded form of input"), + decomposed = AttrMapValue(isString,desc="decomposed form of input"), + canv = AttrMapValue(None,desc="temporarily used for internal methods"), + gap = AttrMapValue(isNumberOrNone, desc='Width of inter character gaps.'), + ) + + barStrokeColor = barFillColor = textColor = black + barStrokeWidth = 0 + _BCC = None + def __init__(self,BCC=None,_value='',**kw): + self._BCC = BCC + class Combiner(self.__class__,BCC): + __name__ = self.__class__.__name__ + self.__class__ = Combiner + PlotArea.__init__(self) + self.x = self.y = 0 + kw.setdefault('value',_value) + BCC.__init__(self,**kw) + + def rect(self,x,y,w,h,**kw): + self._Gadd(Rect(self.x+x,self.y+y,w,h, + strokeColor=self.barStrokeColor,strokeWidth=self.barStrokeWidth, fillColor=self.barFillColor)) + + def draw(self): + if not self._BCC: raise NotImplementedError("Abstract class %s cannot be drawn" % self.__class__.__name__) + self.canv = self + G = Group() + self._Gadd = G.add + self._Gadd(Rect(self.x,self.y,self.width,self.height,fillColor=None,strokeColor=None,strokeWidth=0.0001)) + self._BCC.draw(self) + del self.canv, self._Gadd + return G + + def annotate(self,x,y,text,fontName,fontSize,anchor='middle'): + self._Gadd(String(self.x+x,self.y+y,text,fontName=fontName,fontSize=fontSize, + textAnchor=anchor,fillColor=self.textColor)) + +class BarcodeI2of5(_BarcodeWidget): + """Interleaved 2 of 5 is used in distribution and warehouse industries. + + It encodes an even-numbered sequence of numeric digits. There is an optional + module 10 check digit; if including this, the total length must be odd so that + it becomes even after including the check digit. Otherwise the length must be + even. Since the check digit is optional, our library does not check it. + """ + + _tests = [ + '12', + '1234', + '123456', + '12345678', + '1234567890' + ] + codeName = "I2of5" + _attrMap = AttrMap(BASE=_BarcodeWidget, + barWidth = AttrMapValue(isNumber,'''(float, default .0075): + X-Dimension, or width of the smallest element + Minumum is .0075 inch (7.5 mils).'''), + ratio = AttrMapValue(isNumber,'''(float, default 2.2): + The ratio of wide elements to narrow elements. + Must be between 2.0 and 3.0 (or 2.2 and 3.0 if the + barWidth is greater than 20 mils (.02 inch))'''), + gap = AttrMapValue(isNumberOrNone,'''(float or None, default None): + width of intercharacter gap. None means "use barWidth".'''), + barHeight = AttrMapValue(isNumber,'''(float, see default below): + Height of the symbol. Default is the height of the two + bearer bars (if they exist) plus the greater of .25 inch + or .15 times the symbol's length.'''), + checksum = AttrMapValue(isBoolean,'''(bool, default 1): + Whether to compute and include the check digit'''), + bearers = AttrMapValue(isNumber,'''(float, in units of barWidth. default 3.0): + Height of bearer bars (horizontal bars along the top and + bottom of the barcode). Default is 3 x-dimensions. + Set to zero for no bearer bars. (Bearer bars help detect + misscans, so it is suggested to leave them on).'''), + quiet = AttrMapValue(isBoolean,'''(bool, default 1): + Whether to include quiet zones in the symbol.'''), + + lquiet = AttrMapValue(isNumber,'''(float, see default below): + Quiet zone size to left of code, if quiet is true. + Default is the greater of .25 inch, or .15 times the symbol's + length.'''), + + rquiet = AttrMapValue(isNumber,'''(float, defaults as above): + Quiet zone size to right left of code, if quiet is true.'''), + fontName = AttrMapValue(isString, desc='human readable font'), + fontSize = AttrMapValue(isNumber, desc='human readable font size'), + humanReadable = AttrMapValue(isBoolean, desc='if human readable'), + stop = AttrMapValue(isBoolean, desc='if we use start/stop symbols (default 1)'), + ) + _bcTransMap = {} + + def __init__(self,**kw): + from reportlab.graphics.barcode.common import I2of5 + _BarcodeWidget.__init__(self,I2of5,1234,**kw) + +class BarcodeCode128(BarcodeI2of5): + """Code 128 encodes any number of characters in the ASCII character set. + """ + _tests = [ + 'ReportLab Rocks!' + ] + codeName = "Code128" + _attrMap = AttrMap(BASE=BarcodeI2of5,UNWANTED=('bearers','checksum','ratio','checksum','stop')) + def __init__(self,**kw): + from reportlab.graphics.barcode.code128 import Code128 + _BarcodeWidget.__init__(self,Code128,"AB-12345678",**kw) + +class BarcodeStandard93(BarcodeCode128): + """This is a compressed form of Code 39""" + codeName = "Standard93" + _attrMap = AttrMap(BASE=BarcodeCode128, + stop = AttrMapValue(isBoolean, desc='if we use start/stop symbols (default 1)'), + ) + def __init__(self,**kw): + from reportlab.graphics.barcode.code93 import Standard93 + _BarcodeWidget.__init__(self,Standard93,"CODE 93",**kw) + +class BarcodeExtended93(BarcodeStandard93): + """This is a compressed form of Code 39, allowing the full ASCII charset""" + codeName = "Extended93" + def __init__(self,**kw): + from reportlab.graphics.barcode.code93 import Extended93 + _BarcodeWidget.__init__(self,Extended93,"L@@K! Code 93 ;-)",**kw) + +class BarcodeStandard39(BarcodeI2of5): + """Code39 is widely used in non-retail, especially US defence and health. + Allowed characters are 0-9, A-Z (caps only), space, and -.$/+%*. + """ + + codeName = "Standard39" + def __init__(self,**kw): + from reportlab.graphics.barcode.code39 import Standard39 + _BarcodeWidget.__init__(self,Standard39,"A012345B%R",**kw) + +class BarcodeExtended39(BarcodeI2of5): + """Extended 39 encodes the full ASCII character set by encoding + characters as pairs of Code 39 characters; $, /, % and + are used as + shift characters.""" + + codeName = "Extended39" + def __init__(self,**kw): + from reportlab.graphics.barcode.code39 import Extended39 + _BarcodeWidget.__init__(self,Extended39,"A012345B}",**kw) + +class BarcodeMSI(BarcodeI2of5): + """MSI is used for inventory control in retail applications. + + There are several methods for calculating check digits so we + do not implement one. + """ + codeName = "MSI" + def __init__(self,**kw): + from reportlab.graphics.barcode.common import MSI + _BarcodeWidget.__init__(self,MSI,1234,**kw) + +class BarcodeCodabar(BarcodeI2of5): + """Used in blood banks, photo labs and FedEx labels. + Encodes 0-9, -$:/.+, and four start/stop characters A-D. + """ + codeName = "Codabar" + def __init__(self,**kw): + from reportlab.graphics.barcode.common import Codabar + _BarcodeWidget.__init__(self,Codabar,"A012345B",**kw) + +class BarcodeCode11(BarcodeI2of5): + """Used mostly for labelling telecommunications equipment. + It encodes numeric digits. + """ + codeName = "Code11" + _attrMap = AttrMap(BASE=BarcodeI2of5, + checksum = AttrMapValue(isInt,'''(integer, default 2): + Whether to compute and include the check digit(s). + (0 none, 1 1-digit, 2 2-digit, -1 auto, default -1): + How many checksum digits to include. -1 ("auto") means + 1 if the number of digits is 10 or less, else 2.'''), + ) + def __init__(self,**kw): + from reportlab.graphics.barcode.common import Code11 + _BarcodeWidget.__init__(self,Code11,"01234545634563",**kw) + +class BarcodeFIM(_BarcodeWidget): + """ + FIM was developed as part of the POSTNET barcoding system. FIM (Face Identification Marking) is used by the cancelling machines to sort mail according to whether or not they have bar code and their postage requirements. There are four types of FIM called FIM A, FIM B, FIM C, and FIM D. + + The four FIM types have the following meanings: + FIM A- Postage required pre-barcoded + FIM B - Postage pre-paid, no bar code exists + FIM C- Postage prepaid prebarcoded + FIM D- Postage required, no bar code exists + """ + codeName = "FIM" + _attrMap = AttrMap(BASE=_BarcodeWidget, + barWidth = AttrMapValue(isNumber,'''(float, default 1/32in): the bar width.'''), + spaceWidth = AttrMapValue(isNumber,'''(float or None, default 1/16in): + width of intercharacter gap. None means "use barWidth".'''), + barHeight = AttrMapValue(isNumber,'''(float, default 5/8in): The bar height.'''), + quiet = AttrMapValue(isBoolean,'''(bool, default 0): + Whether to include quiet zones in the symbol.'''), + lquiet = AttrMapValue(isNumber,'''(float, default: 15/32in): + Quiet zone size to left of code, if quiet is true.'''), + rquiet = AttrMapValue(isNumber,'''(float, default 1/4in): + Quiet zone size to right left of code, if quiet is true.'''), + fontName = AttrMapValue(isString, desc='human readable font'), + fontSize = AttrMapValue(isNumber, desc='human readable font size'), + humanReadable = AttrMapValue(isBoolean, desc='if human readable'), + ) + def __init__(self,**kw): + from reportlab.graphics.barcode.usps import FIM + _BarcodeWidget.__init__(self,FIM,"A",**kw) + +class BarcodePOSTNET(_BarcodeWidget): + codeName = "POSTNET" + _attrMap = AttrMap(BASE=_BarcodeWidget, + barWidth = AttrMapValue(isNumber,'''(float, default 0.018*in): the bar width.'''), + spaceWidth = AttrMapValue(isNumber,'''(float or None, default 0.0275in): width of intercharacter gap.'''), + shortHeight = AttrMapValue(isNumber,'''(float, default 0.05in): The short bar height.'''), + barHeight = AttrMapValue(isNumber,'''(float, default 0.125in): The full bar height.'''), + fontName = AttrMapValue(isString, desc='human readable font'), + fontSize = AttrMapValue(isNumber, desc='human readable font size'), + humanReadable = AttrMapValue(isBoolean, desc='if human readable'), + ) + def __init__(self,**kw): + from reportlab.graphics.barcode.usps import POSTNET + _BarcodeWidget.__init__(self,POSTNET,"78247-1043",**kw) + +if __name__=='__main__': + import os, sys, glob + from reportlab.graphics.shapes import Drawing + os.chdir(os.path.dirname(sys.argv[0])) + if not os.path.isdir('out'): + os.mkdir('out') + map(os.remove,glob.glob(os.path.join('out','*'))) + html = [''] + a = html.append + for C in (BarcodeI2of5, + BarcodeCode128, + BarcodeStandard93, + BarcodeExtended93, + BarcodeStandard39, + BarcodeExtended39, + BarcodeMSI, + BarcodeCodabar, + BarcodeCode11, + BarcodeFIM, + BarcodePOSTNET, + ): + name = C.__name__ + i = C() + D = Drawing(100,50) + D.add(i) + D.save(formats=['gif','pict'],outDir='out',fnRoot=name) + a('

%s


' % (name, name)) + a('') + open(os.path.join('out','index.html'),'w').write('\n'.join(html)) diff --git a/bin/reportlab/graphics/charts/__init__.py b/bin/reportlab/graphics/charts/__init__.py new file mode 100644 index 00000000000..6b2a42748cc --- /dev/null +++ b/bin/reportlab/graphics/charts/__init__.py @@ -0,0 +1,4 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/__init__.py +__version__=''' $Id: __init__.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' \ No newline at end of file diff --git a/bin/reportlab/graphics/charts/areas.py b/bin/reportlab/graphics/charts/areas.py new file mode 100644 index 00000000000..7588d924009 --- /dev/null +++ b/bin/reportlab/graphics/charts/areas.py @@ -0,0 +1,92 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/areas.py +"""This module defines a Area mixin classes +""" +__version__=''' $Id: areas.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' +from reportlab.lib.validators import isNumber, isColor, isColorOrNone, isNoneOrShape +from reportlab.graphics.widgetbase import Widget +from reportlab.graphics.shapes import Rect, Group, Line, Polygon +from reportlab.lib.attrmap import AttrMap, AttrMapValue + +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.'), + 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 diff --git a/bin/reportlab/graphics/charts/axes.py b/bin/reportlab/graphics/charts/axes.py new file mode 100644 index 00000000000..ab0be2ad060 --- /dev/null +++ b/bin/reportlab/graphics/charts/axes.py @@ -0,0 +1,1979 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/axes.py +"""Collection of axes for charts. + +The current collection comprises axes for charts using cartesian +coordinate systems. All axes might have tick marks and labels. +There are two dichotomies for axes: one of X and Y flavours and +another of category and value flavours. + +Category axes have an ordering but no metric. They are divided +into a number of equal-sized buckets. Their tick marks or labels, +if available, go BETWEEN the buckets, and the labels are placed +below to/left of the X/Y-axis, respectively. + + Value axes have an ordering AND metric. They correspond to a nu- + meric quantity. Value axis have a real number quantity associated + with it. The chart tells it where to go. + The most basic axis divides the number line into equal spaces + and has tickmarks and labels associated with each; later we + will add variants where you can specify the sampling + interval. + +The charts using axis tell them where the labels should be placed. + +Axes of complementary X/Y flavours can be connected to each other +in various ways, i.e. with a specific reference point, like an +x/value axis to a y/value (or category) axis. In this case the +connection can be either at the top or bottom of the former or +at any absolute value (specified in points) or at some value of +the former axes in its own coordinate system. +""" +__version__=''' $Id: axes.py 2838 2006-04-18 17:47:54Z rgbecker $ ''' + +import string +from types import FunctionType, StringType, TupleType, ListType + +from reportlab.lib.validators import isNumber, isNumberOrNone, isListOfStringsOrNone, isListOfNumbers, \ + isListOfNumbersOrNone, isColorOrNone, OneOf, isBoolean, SequenceOf, \ + isString, EitherOr +from reportlab.lib.attrmap import * +from reportlab.lib import normalDate +from reportlab.graphics.shapes import Drawing, Line, PolyLine, Group, STATE_DEFAULTS, _textBoxLimits, _rotatedBoxLimits +from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection +from reportlab.graphics.charts.textlabels import Label +from reportlab.graphics.charts.utils import nextRoundNumber + + +# Helpers. + +def _findMinMaxValue(V, x, default, func, special=None): + if type(V[0][0]) in (TupleType,ListType): + if special: + f=lambda T,x=x,special=special,func=func: special(T,x,func) + else: + f=lambda T,x=x: T[x] + V=map(lambda e,f=f: map(f,e),V) + V = filter(len,map(lambda x: filter(lambda x: x is not None,x),V)) + if len(V)==0: return default + return func(map(func,V)) + +def _findMin(V, x, default,special=None): + '''find minimum over V[i][x]''' + return _findMinMaxValue(V,x,default,min,special=special) + +def _findMax(V, x, default,special=None): + '''find maximum over V[i][x]''' + return _findMinMaxValue(V,x,default,max,special=special) + +def _allInt(values): + '''true if all values are int''' + for v in values: + try: + if int(v)!=v: return 0 + except: + return 0 + return 1 + +class _AxisG(Widget): + def _get_line_pos(self,v): + v = self.scale(v) + try: + v = v[0] + except: + pass + return v + + def _cxLine(self,x,start,end): + x = self._get_line_pos(x) + return Line(x, self._y + start, x, self._y + end) + + def _cyLine(self,y,start,end): + y = self._get_line_pos(y) + return Line(self._x + start, y, self._x + end, y) + + def _cxLine3d(self,x,start,end,_3d_dx,_3d_dy): + x = self._get_line_pos(x) + y0 = self._y + start + y1 = self._y + end + y0, y1 = min(y0,y1),max(y0,y1) + x1 = x + _3d_dx + return PolyLine([x,y0,x1,y0+_3d_dy,x1,y1+_3d_dy],strokeLineJoin=1) + + def _cyLine3d(self,y,start,end,_3d_dx,_3d_dy): + y = self._get_line_pos(y) + x0 = self._x + start + x1 = self._x + end + x0, x1 = min(x0,x1),max(x0,x1) + y1 = y + _3d_dy + return PolyLine([x0,y,x0+_3d_dx,y1,x1+_3d_dx,y1],strokeLineJoin=1) + + def _getLineFunc(self, start, end, parent=None): + _3d_dx = getattr(parent,'_3d_dx',None) + if _3d_dx is not None: + _3d_dy = getattr(parent,'_3d_dy',None) + f = self._dataIndex and self._cyLine3d or self._cxLine3d + return lambda v, s=start, e=end, f=f,_3d_dx=_3d_dx,_3d_dy=_3d_dy: f(v,s,e,_3d_dx=_3d_dx,_3d_dy=_3d_dy) + else: + f = self._dataIndex and self._cyLine or self._cxLine + return lambda v, s=start, e=end, f=f: f(v,s,e) + + def _makeLines(self,g,start,end,strokeColor,strokeWidth,strokeDashArray,parent=None): + func = self._getLineFunc(start,end,parent) + for t in self._tickValues: + L = func(t) + L.strokeColor = strokeColor + L.strokeWidth = strokeWidth + L.strokeDashArray = strokeDashArray + g.add(L) + + def makeGrid(self,g,dim=None,parent=None): + '''this is only called by a container object''' + s = self.gridStart + e = self.gridEnd + if dim: + s = s is None and dim[0] + e = e is None and dim[0]+dim[1] + c = self.gridStrokeColor + if self.visibleGrid and (s or e) and c is not None: + if self._dataIndex: offs = self._x + else: offs = self._y + self._makeLines(g,s-offs,e-offs,c,self.gridStrokeWidth,self.gridStrokeDashArray,parent=parent) + +# Category axes. +class CategoryAxis(_AxisG): + "Abstract category axis, unusable in itself." + _nodoc = 1 + _attrMap = AttrMap( + visible = AttrMapValue(isBoolean, desc='Display entire object, if true.'), + visibleAxis = AttrMapValue(isBoolean, desc='Display axis line, if true.'), + visibleTicks = AttrMapValue(isBoolean, desc='Display axis ticks, if true.'), + visibleLabels = AttrMapValue(isBoolean, desc='Display axis labels, if true.'), + visibleGrid = AttrMapValue(isBoolean, desc='Display axis grid, if true.'), + strokeWidth = AttrMapValue(isNumber, desc='Width of axis line and ticks.'), + strokeColor = AttrMapValue(isColorOrNone, desc='Color of axis line and ticks.'), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array used for axis line.'), + gridStrokeWidth = AttrMapValue(isNumber, desc='Width of grid lines.'), + gridStrokeColor = AttrMapValue(isColorOrNone, desc='Color of grid lines.'), + gridStrokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array used for grid lines.'), + gridStart = AttrMapValue(isNumberOrNone, desc='Start of grid lines wrt axis origin'), + gridEnd = AttrMapValue(isNumberOrNone, desc='End of grid lines wrt axis origin'), + labels = AttrMapValue(None, desc='Handle of the axis labels.'), + categoryNames = AttrMapValue(isListOfStringsOrNone, desc='List of category names.'), + joinAxis = AttrMapValue(None, desc='Join both axes if true.'), + joinAxisPos = AttrMapValue(isNumberOrNone, desc='Position at which to join with other axis.'), + reverseDirection = AttrMapValue(isBoolean, desc='If true reverse category direction.'), + style = AttrMapValue(OneOf('parallel','stacked','parallel_3d'),"How common category bars are plotted"), + labelAxisMode = AttrMapValue(OneOf('high','low','axis'), desc="Like joinAxisMode, but for the axis labels"), + tickShift = AttrMapValue(isBoolean, desc='Tick shift typically'), + ) + + def __init__(self): + assert self.__class__.__name__!='CategoryAxis', "Abstract Class CategoryAxis Instantiated" + # private properties set by methods. The initial values + # here are to make demos easy; they would always be + # overridden in real life. + self._x = 50 + self._y = 50 + self._length = 100 + self._catCount = 0 + + # public properties + self.visible = 1 + self.visibleAxis = 1 + self.visibleTicks = 1 + self.visibleLabels = 1 + self.visibleGrid = 0 + + self.strokeWidth = 1 + self.strokeColor = STATE_DEFAULTS['strokeColor'] + self.strokeDashArray = STATE_DEFAULTS['strokeDashArray'] + self.gridStrokeWidth = 0.25 + self.gridStrokeColor = STATE_DEFAULTS['strokeColor'] + self.gridStrokeDashArray = STATE_DEFAULTS['strokeDashArray'] + self.gridStart = self.gridEnd = 0 + self.labels = TypedPropertyCollection(Label) + # if None, they don't get labels. If provided, + # you need one name per data point and they are + # used for label text. + self.categoryNames = None + self.joinAxis = None + self.joinAxisPos = None + self.joinAxisMode = None + self.labelAxisMode = 'axis' + self.reverseDirection = 0 + self.style = 'parallel' + + #various private things which need to be initialized + self._labelTextFormat = None + self.tickShift = 0 + + def setPosition(self, x, y, length): + # ensure floating point + self._x = x + self._y = y + self._length = length + + + def configure(self, multiSeries,barWidth=None): + self._catCount = max(map(len,multiSeries)) + self._barWidth = barWidth or (self._length/float(self._catCount or 1)) + self._calcTickmarkPositions() + + def _calcTickmarkPositions(self): + n = self._catCount + if self.tickShift: + self._tickValues = [t+0.5 for t in xrange(n)] + else: + self._tickValues = range(n+1) + + def draw(self): + g = Group() + + if not self.visible: + return g + + g.add(self.makeAxis()) + g.add(self.makeTicks()) + g.add(self.makeTickLabels()) + + return g + + def _scale(self,idx): + if self.reverseDirection: idx = self._catCount-idx-1 + return idx + +def _assertYAxis(axis): + acn = axis.__class__.__name__ + assert acn[0]=='Y' or acn[:4]=='AdjY', "Cannot connect to other axes (%s), but Y- ones." % acn +def _assertXAxis(axis): + acn = axis.__class__.__name__ + assert acn[0]=='X' or acn[:11]=='NormalDateX', "Cannot connect to other axes (%s), but X- ones." % acn + +class XCategoryAxis(CategoryAxis): + "X/category axis" + + _attrMap = AttrMap(BASE=CategoryAxis, + tickUp = AttrMapValue(isNumber, + desc='Tick length up the axis.'), + tickDown = AttrMapValue(isNumber, + desc='Tick length down the axis.'), + joinAxisMode = AttrMapValue(OneOf('bottom', 'top', 'value', 'points', None), + desc="Mode used for connecting axis ('bottom', 'top', 'value', 'points', None)."), + ) + + _dataIndex = 0 + + def __init__(self): + CategoryAxis.__init__(self) + self.labels.boxAnchor = 'n' #north - top edge + self.labels.dy = -5 + # ultra-simple tick marks for now go between categories + # and have same line style as axis - need more + self.tickUp = 0 # how far into chart does tick go? + self.tickDown = 5 # how far below axis does tick go? + + + def demo(self): + self.setPosition(30, 70, 140) + self.configure([(10,20,30,40,50)]) + + self.categoryNames = ['One','Two','Three','Four','Five'] + # all labels top-centre aligned apart from the last + self.labels.boxAnchor = 'n' + self.labels[4].boxAnchor = 'e' + self.labels[4].angle = 90 + + d = Drawing(200, 100) + d.add(self) + return d + + + def joinToAxis(self, yAxis, mode='bottom', pos=None): + "Join with y-axis using some mode." + + _assertYAxis(yAxis) + if mode == 'bottom': + self._x = yAxis._x + self._y = yAxis._y + elif mode == 'top': + self._x = yAxis._x + self._y = yAxis._y + yAxis._length + elif mode == 'value': + self._x = yAxis._x + self._y = yAxis.scale(pos) + elif mode == 'points': + self._x = yAxis._x + self._y = pos + + def _joinToAxis(self): + ja = self.joinAxis + if ja: + jam = self.joinAxisMode + jap = self.joinAxisPos + jta = self.joinToAxis + if jam in ('bottom', 'top'): + jta(ja, mode=jam) + elif jam in ('value', 'points'): + jta(ja, mode=jam, pos=jap) + + def scale(self, idx): + """returns the x position and width in drawing units of the slice""" + return (self._x + self._scale(idx)*self._barWidth, self._barWidth) + + def makeAxis(self): + g = Group() + self._joinToAxis() + if not self.visibleAxis: return g + + axis = Line(self._x, self._y, self._x + self._length, self._y) + axis.strokeColor = self.strokeColor + axis.strokeWidth = self.strokeWidth + axis.strokeDashArray = self.strokeDashArray + g.add(axis) + + return g + + def makeTicks(self): + g = Group() + if not self.visibleTicks: return g + if self.tickUp or self.tickDown: + self._makeLines(g,self.tickUp,-self.tickDown,self.strokeColor,self.strokeWidth,self.strokeDashArray) + return g + + def _labelAxisPos(self): + axis = self.joinAxis + if axis: + mode = self.labelAxisMode + if mode == 'low': + return axis._y + elif mode == 'high': + return axis._y + axis._length + return self._y + + def makeTickLabels(self): + g = Group() + + if not self.visibleLabels: return g + + categoryNames = self.categoryNames + if categoryNames is not None: + catCount = self._catCount + n = len(categoryNames) + reverseDirection = self.reverseDirection + barWidth = self._barWidth + _y = self._labelAxisPos() + _x = self._x + + for i in xrange(catCount): + if reverseDirection: ic = catCount-i-1 + else: ic = i + if ic>=n: continue + x = _x + (i+0.5) * barWidth + label = self.labels[i] + label.setOrigin(x, _y) + label.setText(categoryNames[ic] or '') + g.add(label) + + return g + + +class YCategoryAxis(CategoryAxis): + "Y/category axis" + + _attrMap = AttrMap(BASE=CategoryAxis, + tickLeft = AttrMapValue(isNumber, + desc='Tick length left of the axis.'), + tickRight = AttrMapValue(isNumber, + desc='Tick length right of the axis.'), + joinAxisMode = AttrMapValue(OneOf(('left', 'right', 'value', 'points', None)), + desc="Mode used for connecting axis ('left', 'right', 'value', 'points', None)."), + ) + + _dataIndex = 1 + + + def __init__(self): + CategoryAxis.__init__(self) + self.labels.boxAnchor = 'e' #east - right edge + self.labels.dx = -5 + # ultra-simple tick marks for now go between categories + # and have same line style as axis - need more + self.tickLeft = 5 # how far left of axis does tick go? + self.tickRight = 0 # how far right of axis does tick go? + + + def demo(self): + self.setPosition(50, 10, 80) + self.configure([(10,20,30)]) + self.categoryNames = ['One','Two','Three'] + # all labels top-centre aligned apart from the last + self.labels.boxAnchor = 'e' + self.labels[2].boxAnchor = 's' + self.labels[2].angle = 90 + + d = Drawing(200, 100) + d.add(self) + return d + + + def joinToAxis(self, xAxis, mode='left', pos=None): + "Join with x-axis using some mode." + + _assertXAxis(xAxis) + + if mode == 'left': + self._x = xAxis._x * 1.0 + self._y = xAxis._y * 1.0 + elif mode == 'right': + self._x = (xAxis._x + xAxis._length) * 1.0 + self._y = xAxis._y * 1.0 + elif mode == 'value': + self._x = xAxis.scale(pos) * 1.0 + self._y = xAxis._y * 1.0 + elif mode == 'points': + self._x = pos * 1.0 + self._y = xAxis._y * 1.0 + + def _joinToAxis(self): + ja = self.joinAxis + if ja: + jam = self.joinAxisMode + jap = self.joinAxisPos + jta = self.joinToAxis + if jam in ('left', 'right'): + jta(ja, mode=jam) + elif jam in ('value', 'points'): + jta(ja, mode=jam, pos=jap) + + def scale(self, idx): + "Returns the y position and width in drawing units of the slice." + return (self._y + self._scale(idx)*self._barWidth, self._barWidth) + + def makeAxis(self): + g = Group() + self._joinToAxis() + if not self.visibleAxis: return g + + axis = Line(self._x, self._y, self._x, self._y + self._length) + axis.strokeColor = self.strokeColor + axis.strokeWidth = self.strokeWidth + axis.strokeDashArray = self.strokeDashArray + g.add(axis) + + return g + + def makeTicks(self): + g = Group() + if not self.visibleTicks: return g + if self.tickLeft or self.tickRight: + self._makeLines(g,-self.tickLeft,self.tickRight,self.strokeColor,self.strokeWidth,self.strokeDashArray) + return g + + def _labelAxisPos(self): + axis = self.joinAxis + if axis: + mode = self.labelAxisMode + if mode == 'low': + return axis._x + elif mode == 'high': + return axis._x + axis._length + return self._x + + def makeTickLabels(self): + g = Group() + + if not self.visibleTicks: return g + + categoryNames = self.categoryNames + if categoryNames is not None: + catCount = self._catCount + n = len(categoryNames) + reverseDirection = self.reverseDirection + barWidth = self._barWidth + labels = self.labels + _x = self._labelAxisPos() + _y = self._y + for i in xrange(catCount): + if reverseDirection: ic = catCount-i-1 + else: ic = i + if ic>=n: continue + y = _y + (i+0.5) * barWidth + label = labels[i] + label.setOrigin(_x, y) + label.setText(categoryNames[ic] or '') + g.add(label) + return g + + +# Value axes. +class ValueAxis(_AxisG): + "Abstract value axis, unusable in itself." + + _attrMap = AttrMap( + forceZero = AttrMapValue(EitherOr((isBoolean,OneOf('near'))), desc='Ensure zero in range if true.'), + visible = AttrMapValue(isBoolean, desc='Display entire object, if true.'), + visibleAxis = AttrMapValue(isBoolean, desc='Display axis line, if true.'), + visibleLabels = AttrMapValue(isBoolean, desc='Display axis labels, if true.'), + visibleTicks = AttrMapValue(isBoolean, desc='Display axis ticks, if true.'), + visibleGrid = AttrMapValue(isBoolean, desc='Display axis grid, if true.'), + strokeWidth = AttrMapValue(isNumber, desc='Width of axis line and ticks.'), + strokeColor = AttrMapValue(isColorOrNone, desc='Color of axis line and ticks.'), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array used for axis line.'), + gridStrokeWidth = AttrMapValue(isNumber, desc='Width of grid lines.'), + gridStrokeColor = AttrMapValue(isColorOrNone, desc='Color of grid lines.'), + gridStrokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array used for grid lines.'), + gridStart = AttrMapValue(isNumberOrNone, desc='Start of grid lines wrt axis origin'), + gridEnd = AttrMapValue(isNumberOrNone, desc='End of grid lines wrt axis origin'), + minimumTickSpacing = AttrMapValue(isNumber, desc='Minimum value for distance between ticks.'), + maximumTicks = AttrMapValue(isNumber, desc='Maximum number of ticks.'), + labels = AttrMapValue(None, desc='Handle of the axis labels.'), + labelTextFormat = AttrMapValue(None, desc='Formatting string or function used for axis labels.'), + labelTextPostFormat = AttrMapValue(None, desc='Extra Formatting string.'), + labelTextScale = AttrMapValue(isNumberOrNone, desc='Scaling for label tick values.'), + valueMin = AttrMapValue(isNumberOrNone, desc='Minimum value on axis.'), + valueMax = AttrMapValue(isNumberOrNone, desc='Maximum value on axis.'), + valueStep = AttrMapValue(isNumberOrNone, desc='Step size used between ticks.'), + valueSteps = AttrMapValue(isListOfNumbersOrNone, desc='List of step sizes used between ticks.'), + avoidBoundFrac = AttrMapValue(EitherOr((isNumberOrNone,SequenceOf(isNumber,emptyOK=0,lo=2,hi=2))), desc='Fraction of interval to allow above and below.'), + rangeRound=AttrMapValue(OneOf('none','both','ceiling','floor'),'How to round the axis limits'), + zrangePref = AttrMapValue(isNumberOrNone, desc='Zero range axis limit preference.'), + style = AttrMapValue(OneOf('normal','stacked','parallel_3d'),"How values are plotted!"), + ) + + def __init__(self): + assert self.__class__.__name__!='ValueAxis', 'Abstract Class ValueAxis Instantiated' + self._configured = 0 + # private properties set by methods. The initial values + # here are to make demos easy; they would always be + # overridden in real life. + self._x = 50 + self._y = 50 + self._length = 100 + + # public properties + self.visible = 1 + self.visibleAxis = 1 + self.visibleLabels = 1 + self.visibleTicks = 1 + self.visibleGrid = 0 + self.forceZero = 0 + + self.strokeWidth = 1 + self.strokeColor = STATE_DEFAULTS['strokeColor'] + self.strokeDashArray = STATE_DEFAULTS['strokeDashArray'] + self.gridStrokeWidth = 0.25 + self.gridStrokeColor = STATE_DEFAULTS['strokeColor'] + self.gridStrokeDashArray = STATE_DEFAULTS['strokeDashArray'] + self.gridStart = self.gridEnd = 0 + + self.labels = TypedPropertyCollection(Label) + self.labels.angle = 0 + + # how close can the ticks be? + self.minimumTickSpacing = 10 + self.maximumTicks = 7 + + # a format string like '%0.2f' + # or a function which takes the value as an argument and returns a string + self._labelTextFormat = self.labelTextFormat = self.labelTextPostFormat = self.labelTextScale = None + + # if set to None, these will be worked out for you. + # if you override any or all of them, your values + # will be used. + self.valueMin = None + self.valueMax = None + self.valueStep = None + self.avoidBoundFrac = None + self.rangeRound = 'none' + self.zrangePref = 0 + self.style = 'normal' + + def setPosition(self, x, y, length): + # ensure floating point + self._x = x * 1.0 + self._y = y * 1.0 + self._length = length * 1.0 + + def configure(self, dataSeries): + """Let the axis configure its scale and range based on the data. + + Called after setPosition. Let it look at a list of lists of + numbers determine the tick mark intervals. If valueMin, + valueMax and valueStep are configured then it + will use them; if any of them are set to None it + will look at the data and make some sensible decision. + You may override this to build custom axes with + irregular intervals. It creates an internal + variable self._values, which is a list of numbers + to use in plotting. + """ + self._setRange(dataSeries) + self._calcTickmarkPositions() + self._calcScaleFactor() + self._configured = 1 + + def _getValueStepAndTicks(self, valueMin, valueMax,cache={}): + try: + K = (valueMin,valueMax) + r = cache[K] + except: + self._valueMin = valueMin + self._valueMax = valueMax + T = self._calcTickPositions() + if len(T)>1: + valueStep = T[1]-T[0] + else: + oVS = self.valueStep + self.valueStep = None + T = self._calcTickPositions() + self.valueStep = oVS + if len(T)>1: + valueStep = T[1]-T[0] + else: + valueStep = self._valueStep + r = cache[K] = valueStep, T, valueStep*1e-8 + return r + + def _setRange(self, dataSeries): + """Set minimum and maximum axis values. + + The dataSeries argument is assumed to be a list of data + vectors. Each vector is itself a list or tuple of numbers. + + Returns a min, max tuple. + """ + + oMin = valueMin = self.valueMin + oMax = valueMax = self.valueMax + rangeRound = self.rangeRound + if valueMin is None: valueMin = self._cValueMin = _findMin(dataSeries,self._dataIndex,0) + if valueMax is None: valueMax = self._cValueMax = _findMax(dataSeries,self._dataIndex,0) + if valueMin == valueMax: + if valueMax==0: + if oMin is None and oMax is None: + zrp = getattr(self,'zrangePref',0) + if zrp>0: + valueMax = zrp + valueMin = 0 + elif zrp<0: + valueMax = 0 + valueMin = zrp + else: + valueMax = 0.01 + valueMin = -0.01 + elif self.valueMin is None: + valueMin = -0.01 + else: + valueMax = 0.01 + else: + if valueMax>0: + valueMax = 1.2*valueMax + valueMin = 0.0 + else: + valueMax = 0.0 + valueMin = 1.2*valueMin + + if getattr(self,'_bubblePlot',None): + bubbleMax = float(_findMax(dataSeries,2,0)) + frac=.25 + bubbleV=frac*(valueMax-valueMin) + self._bubbleV = bubbleV + self._bubbleMax = bubbleMax + self._bubbleRadius = frac*self._length + def special(T,x,func,bubbleV=bubbleV,bubbleMax=bubbleMax): + try: + v = T[2] + except IndexError: + v = bubbleMAx*0.1 + bubbleV *= (v/bubbleMax)**0.5 + return func(T[x]+bubbleV,T[x]-bubbleV) + if oMin is None: valueMin = self._cValueMin = _findMin(dataSeries,self._dataIndex,0,special=special) + if oMax is None: valueMax = self._cValueMax = _findMax(dataSeries,self._dataIndex,0,special=special) + + forceZero = self.forceZero + if forceZero: + if forceZero=='near': + forceZero = min(abs(valueMin),abs(valueMax)) <= 5*(valueMax-valueMin) + if forceZero: + if valueMax<0: valueMax=0 + elif valueMin>0: valueMin = 0 + + abf = self.avoidBoundFrac + do_rr = not getattr(self,'valueSteps',None) + do_abf = abf and do_rr + if type(abf) not in (TupleType,ListType): + abf = abf, abf + do_rr = rangeRound is not 'none' and do_rr + if do_rr: + rrn = rangeRound in ['both','floor'] + rrx = rangeRound in ['both','ceiling'] + else: + rrn = rrx = 0 + + go = do_rr or do_abf + cache = {} + cMin = valueMin + cMax = valueMax + iter = 0 + while go and iter<=10: + iter += 1 + go = 0 + if do_abf: + valueStep, T, fuzz = self._getValueStepAndTicks(valueMin, valueMax, cache) + i0 = valueStep*abf[0] + i1 = valueStep*abf[1] + if rrn: v = T[0] + else: v = valueMin + u = cMin-i0 + if abs(v)>fuzz and v>=u+fuzz: + valueMin = u + go = 1 + if rrx: v = T[-1] + else: v = valueMax + u = cMax+i1 + if abs(v)>fuzz and v<=u-fuzz: + valueMax = u + go = 1 + + if do_rr: + valueStep, T, fuzz = self._getValueStepAndTicks(valueMin, valueMax, cache) + if rrn: + if valueMin=T[0]+fuzz + valueMin = T[0] + if rrx: + if valueMax>T[-1]+fuzz: + valueMax = T[-1]+valueStep + go = 1 + else: + go = valueMax<=T[-1]-fuzz + valueMax = T[-1] + + self._valueMin, self._valueMax = valueMin, valueMax + self._rangeAdjust() + + def _rangeAdjust(self): + """Override this if you want to alter the calculated range. + + E.g. if want a minumamum range of 30% or don't want 100% + as the first point. + """ + pass + + def _adjustAxisTicks(self): + '''Override if you want to put slack at the ends of the axis + eg if you don't want the last tick to be at the bottom etc + ''' + pass + + def _calcScaleFactor(self): + """Calculate the axis' scale factor. + This should be called only *after* the axis' range is set. + Returns a number. + """ + self._scaleFactor = self._length / float(self._valueMax - self._valueMin) + return self._scaleFactor + + def _calcTickPositions(self): + self._calcValueStep() + valueMin, valueMax, valueStep = self._valueMin, self._valueMax, self._valueStep + fuzz = 1e-8*valueStep + rangeRound = self.rangeRound + i0 = int(float(valueMin)/valueStep) + v = i0*valueStep + if rangeRound in ('both','floor'): + if v>valueMin+fuzz: i0 -= 1 + elif vvalueMax+fuzz: i1 -= 1 + return [i*valueStep for i in xrange(i0,i1+1)] + + def _calcTickmarkPositions(self): + """Calculate a list of tick positions on the axis. Returns a list of numbers.""" + self._tickValues = getattr(self,'valueSteps',None) + if self._tickValues: return self._tickValues + self._tickValues = self._calcTickPositions() + self._adjustAxisTicks() + return self._tickValues + + def _calcValueStep(self): + '''Calculate _valueStep for the axis or get from valueStep.''' + if self.valueStep is None: + rawRange = self._valueMax - self._valueMin + rawInterval = rawRange / min(float(self.maximumTicks-1),(float(self._length)/self.minimumTickSpacing)) + self._valueStep = nextRoundNumber(rawInterval) + else: + self._valueStep = self.valueStep + + def _allIntTicks(self): + return _allInt(self._tickValues) + + def makeTickLabels(self): + g = Group() + if not self.visibleLabels: return g + + f = self._labelTextFormat # perhaps someone already set it + if f is None: + f = self.labelTextFormat or (self._allIntTicks() and '%.0f' or str) + elif f is str and self._allIntTicks(): f = '%.0f' + post = self.labelTextPostFormat + scl = self.labelTextScale + pos = [self._x, self._y] + d = self._dataIndex + labels = self.labels + + i = 0 + for tick in self._tickValues: + if f and labels[i].visible: + v = self.scale(tick) + if scl is not None: + t = tick*scl + else: + t = tick + if type(f) is StringType: txt = f % t + elif type(f) in (TupleType,ListType): + #it's a list, use as many items as we get + if i < len(f): + txt = f[i] + else: + txt = '' + elif callable(f): + txt = f(t) + else: + raise ValueError, 'Invalid labelTextFormat %s' % f + if post: txt = post % txt + label = labels[i] + pos[d] = v + apply(label.setOrigin,pos) + label.setText(txt) + g.add(label) + i = i + 1 + + return g + + def draw(self): + g = Group() + + if not self.visible: + return g + + g.add(self.makeAxis()) + g.add(self.makeTicks()) + g.add(self.makeTickLabels()) + + return g + + +class XValueAxis(ValueAxis): + "X/value axis" + + _attrMap = AttrMap(BASE=ValueAxis, + tickUp = AttrMapValue(isNumber, + desc='Tick length up the axis.'), + tickDown = AttrMapValue(isNumber, + desc='Tick length down the axis.'), + joinAxis = AttrMapValue(None, + desc='Join both axes if true.'), + joinAxisMode = AttrMapValue(OneOf('bottom', 'top', 'value', 'points', None), + desc="Mode used for connecting axis ('bottom', 'top', 'value', 'points', None)."), + joinAxisPos = AttrMapValue(isNumberOrNone, + desc='Position at which to join with other axis.'), + ) + + # Indicate the dimension of the data we're interested in. + _dataIndex = 0 + + def __init__(self): + ValueAxis.__init__(self) + + self.labels.boxAnchor = 'n' + self.labels.dx = 0 + self.labels.dy = -5 + + self.tickUp = 0 + self.tickDown = 5 + + self.joinAxis = None + self.joinAxisMode = None + self.joinAxisPos = None + + + def demo(self): + self.setPosition(20, 50, 150) + self.configure([(10,20,30,40,50)]) + + d = Drawing(200, 100) + d.add(self) + return d + + + def joinToAxis(self, yAxis, mode='bottom', pos=None): + "Join with y-axis using some mode." + _assertYAxis(yAxis) + if mode == 'bottom': + self._x = yAxis._x * 1.0 + self._y = yAxis._y * 1.0 + elif mode == 'top': + self._x = yAxis._x * 1.0 + self._y = (yAxis._y + yAxis._length) * 1.0 + elif mode == 'value': + self._x = yAxis._x * 1.0 + self._y = yAxis.scale(pos) * 1.0 + elif mode == 'points': + self._x = yAxis._x * 1.0 + self._y = pos * 1.0 + + def _joinToAxis(self): + ja = self.joinAxis + if ja: + jam = self.joinAxisMode + jap = self.joinAxisPos + jta = self.joinToAxis + if jam in ('bottom', 'top'): + jta(ja, mode=jam) + elif jam in ('value', 'points'): + jta(ja, mode=jam, pos=jap) + + def scale(self, value): + """Converts a numeric value to a Y position. + + The chart first configures the axis, then asks it to + work out the x value for each point when plotting + lines or bars. You could override this to do + logarithmic axes. + """ + + msg = "Axis cannot scale numbers before it is configured" + assert self._configured, msg + if value is None: + value = 0 + return self._x + self._scaleFactor * (value - self._valueMin) + + + def makeAxis(self): + g = Group() + self._joinToAxis() + if not self.visibleAxis: return g + + axis = Line(self._x, self._y, self._x + self._length, self._y) + axis.strokeColor = self.strokeColor + axis.strokeWidth = self.strokeWidth + axis.strokeDashArray = self.strokeDashArray + g.add(axis) + + return g + + def makeTicks(self): + g = Group() + if self.visibleTicks and (self.tickUp or self.tickDown): + self._makeLines(g,-self.tickDown,self.tickUp,self.strokeColor,self.strokeWidth,self.strokeDashArray) + return g + +class NormalDateXValueAxis(XValueAxis): + """An X axis applying additional rules. + + Depending on the data and some built-in rules, the axis + displays normalDate values as nicely formatted dates. + + The client chart should have NormalDate X values. + """ + + _attrMap = AttrMap(BASE = XValueAxis, + bottomAxisLabelSlack = AttrMapValue(isNumber, desc="Fractional amount used to adjust label spacing"), + niceMonth = AttrMapValue(isBoolean, desc="Flag for displaying months 'nicely'."), + forceEndDate = AttrMapValue(isBoolean, desc='Flag for enforced displaying of last date value.'), + forceFirstDate = AttrMapValue(isBoolean, desc='Flag for enforced displaying of first date value.'), + xLabelFormat = AttrMapValue(None, desc="Label format string (e.g. '{mm}/{yy}') or function."), + dayOfWeekName = AttrMapValue(SequenceOf(isString,emptyOK=0,lo=7,hi=7), desc='Weekday names.'), + monthName = AttrMapValue(SequenceOf(isString,emptyOK=0,lo=12,hi=12), desc='Month names.'), + dailyFreq = AttrMapValue(isBoolean, desc='True if we are to assume daily data to be ticked at end of month.'), + ) + + _valueClass = normalDate.ND + + def __init__(self, **kw): + apply(XValueAxis.__init__, (self,)) + + # some global variables still used... + self.bottomAxisLabelSlack = 0.1 + self.niceMonth = 1 + self.forceEndDate = 0 + self.forceFirstDate = 0 + self.dailyFreq = 0 + self.xLabelFormat = "{mm}/{yy}" + self.dayOfWeekName = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] + self.monthName = ['January', 'February', 'March', 'April', 'May', 'June', 'July', + 'August', 'September', 'October', 'November', 'December'] + self.valueSteps = None + + def _scalar2ND(self, x): + "Convert a scalar to a NormalDate value." + d = self._valueClass() + d.normalize(x) + return d + + def _dateFormatter(self, v): + "Create a formatted label for some value." + if not isinstance(v,normalDate.NormalDate): + v = self._scalar2ND(v) + d, m = normalDate._dayOfWeekName, normalDate._monthName + try: + normalDate._dayOfWeekName, normalDate._monthName = self.dayOfWeekName, self.monthName + return v.formatMS(self.xLabelFormat) + finally: + normalDate._dayOfWeekName, normalDate._monthName = d, m + + def _xAxisTicker(self, xVals): + """Complex stuff... + + Needs explanation... + """ + axisLength = self._length + formatter = self._dateFormatter + labels = self.labels + fontName, fontSize, leading = labels.fontName, labels.fontSize, labels.leading + textAnchor, boxAnchor, angle = labels.textAnchor, labels.boxAnchor, labels.angle + RBL = _textBoxLimits(string.split(formatter(xVals[0]),'\n'),fontName, + fontSize,leading or 1.2*fontSize,textAnchor,boxAnchor) + RBL = _rotatedBoxLimits(RBL[0],RBL[1],RBL[2],RBL[3], angle) + xLabelW = RBL[1]-RBL[0] + xLabelH = RBL[3]-RBL[2] + w = max(xLabelW,labels.width,self.minimumTickSpacing) + + W = w+w*self.bottomAxisLabelSlack + n = len(xVals) + ticks = [] + labels = [] + maximumTicks = self.maximumTicks + + def addTick(i, xVals=xVals, formatter=formatter, ticks=ticks, labels=labels): + ticks.insert(0,xVals[i]) + labels.insert(0,formatter(xVals[i])) + + for d in (1,2,3,6,12,24,60,120): + k = n/d + if k<=maximumTicks and k*W <= axisLength: + i = n-1 + if self.niceMonth: + j = xVals[-1].month() % (d<=12 and d or 12) + if j: + if self.forceEndDate: addTick(i) + i = i - j + + #weird first date ie not at end of month + try: + wfd = xVals[0].month() == xVals[1].month() + except: + wfd = 0 + + while i>=wfd: + addTick(i) + i = i - d + + if self.forceFirstDate and ticks[0] != xVals[0]: + addTick(0) + if (axisLength/(ticks[-1]-ticks[0]))*(ticks[1]-ticks[0])<=w: + del ticks[1], labels[1] + if self.forceEndDate and self.niceMonth and j: + if (axisLength/(ticks[-1]-ticks[0]))*(ticks[-1]-ticks[-2])<=w: + del ticks[-2], labels[-2] + try: + if labels[0] and labels[0]==labels[1]: + del ticks[1], labels[1] + except IndexError: + pass + + return ticks, labels + + def _convertXV(self,data): + '''Convert all XValues to a standard normalDate type''' + + VC = self._valueClass + for D in data: + for i in xrange(len(D)): + x, y = D[i] + if not isinstance(x,VC): + D[i] = (VC(x),y) + + def _getStepsAndLabels(self,xVals): + if self.dailyFreq: + xEOM = [] + pm = 0 + px = xVals[0] + for x in xVals: + m = x.month() + if pm!=m: + if pm: xEOM.append(px) + pm = m + px = x + px = xVals[-1] + if xEOM[-1]!=x: xEOM.append(px) + steps, labels = self._xAxisTicker(xEOM) + else: + steps, labels = self._xAxisTicker(xVals) + return steps, labels + + def configure(self, data): + self._convertXV(data) + from reportlab.lib.set_ops import union + xVals = reduce(union,map(lambda x: map(lambda dv: dv[0],x),data),[]) + xVals.sort() + steps,labels = self._getStepsAndLabels(xVals) + valueMin, valueMax = self.valueMin, self.valueMax + if valueMin is None: valueMin = xVals[0] + if valueMax is None: valueMax = xVals[-1] + self._valueMin, self._valueMax = valueMin, valueMax + self._tickValues = steps + self._labelTextFormat = labels + + self._scaleFactor = self._length / float(valueMax - valueMin) + self._tickValues = steps + self._configured = 1 + +class YValueAxis(ValueAxis): + "Y/value axis" + + _attrMap = AttrMap(BASE=ValueAxis, + tickLeft = AttrMapValue(isNumber, + desc='Tick length left of the axis.'), + tickRight = AttrMapValue(isNumber, + desc='Tick length right of the axis.'), + joinAxis = AttrMapValue(None, + desc='Join both axes if true.'), + joinAxisMode = AttrMapValue(OneOf(('left', 'right', 'value', 'points', None)), + desc="Mode used for connecting axis ('left', 'right', 'value', 'points', None)."), + joinAxisPos = AttrMapValue(isNumberOrNone, + desc='Position at which to join with other axis.'), + ) + + # Indicate the dimension of the data we're interested in. + _dataIndex = 1 + + def __init__(self): + ValueAxis.__init__(self) + + self.labels.boxAnchor = 'e' + self.labels.dx = -5 + self.labels.dy = 0 + + self.tickRight = 0 + self.tickLeft = 5 + + self.joinAxis = None + self.joinAxisMode = None + self.joinAxisPos = None + + + def demo(self): + data = [(10, 20, 30, 42)] + self.setPosition(100, 10, 80) + self.configure(data) + + drawing = Drawing(200, 100) + drawing.add(self) + return drawing + + + + def joinToAxis(self, xAxis, mode='left', pos=None): + "Join with x-axis using some mode." + _assertXAxis(xAxis) + if mode == 'left': + self._x = xAxis._x * 1.0 + self._y = xAxis._y * 1.0 + elif mode == 'right': + self._x = (xAxis._x + xAxis._length) * 1.0 + self._y = xAxis._y * 1.0 + elif mode == 'value': + self._x = xAxis.scale(pos) * 1.0 + self._y = xAxis._y * 1.0 + elif mode == 'points': + self._x = pos * 1.0 + self._y = xAxis._y * 1.0 + + def _joinToAxis(self): + ja = self.joinAxis + if ja: + jam = self.joinAxisMode + jap = self.joinAxisPos + jta = self.joinToAxis + if jam in ('left', 'right'): + jta(ja, mode=jam) + elif jam in ('value', 'points'): + jta(ja, mode=jam, pos=jap) + + def scale(self, value): + """Converts a numeric value to a Y position. + + The chart first configures the axis, then asks it to + work out the x value for each point when plotting + lines or bars. You could override this to do + logarithmic axes. + """ + + msg = "Axis cannot scale numbers before it is configured" + assert self._configured, msg + + if value is None: + value = 0 + return self._y + self._scaleFactor * (value - self._valueMin) + + + def makeAxis(self): + g = Group() + self._joinToAxis() + if not self.visibleAxis: return g + + axis = Line(self._x, self._y, self._x, self._y + self._length) + axis.strokeColor = self.strokeColor + axis.strokeWidth = self.strokeWidth + axis.strokeDashArray = self.strokeDashArray + g.add(axis) + + return g + + def makeTicks(self): + g = Group() + if self.visibleTicks and (self.tickLeft or self.tickRight): + self._makeLines(g,-self.tickLeft,self.tickRight,self.strokeColor,self.strokeWidth,self.strokeDashArray) + return g + +class AdjYValueAxis(YValueAxis): + """A Y-axis applying additional rules. + + Depending on the data and some built-in rules, the axis + may choose to adjust its range and origin. + """ + _attrMap = AttrMap(BASE = YValueAxis, + requiredRange = AttrMapValue(isNumberOrNone, desc='Minimum required value range.'), + leftAxisPercent = AttrMapValue(isBoolean, desc='When true add percent sign to label values.'), + leftAxisOrigShiftIPC = AttrMapValue(isNumber, desc='Lowest label shift interval ratio.'), + leftAxisOrigShiftMin = AttrMapValue(isNumber, desc='Minimum amount to shift.'), + leftAxisSkipLL0 = AttrMapValue(EitherOr((isBoolean,isListOfNumbers)), desc='Skip/Keep lowest tick label when true/false.\nOr skiplist') + ) + + def __init__(self): + apply(YValueAxis.__init__, (self,)) + self.requiredRange = 30 + self.leftAxisPercent = 1 + self.leftAxisOrigShiftIPC = 0.15 + self.leftAxisOrigShiftMin = 12 + self.leftAxisSkipLL0 = 0 + self.valueSteps = None + + def _rangeAdjust(self): + "Adjusts the value range of the axis." + + from reportlab.graphics.charts.utils import find_good_grid, ticks + y_min, y_max = self._valueMin, self._valueMax + m = self.maximumTicks + n = filter(lambda x,m=m: x<=m,[4,5,6,7,8,9]) + if not n: n = [m] + + valueStep, requiredRange = self.valueStep, self.requiredRange + if requiredRange and y_max - y_min < requiredRange: + y1, y2 = find_good_grid(y_min, y_max,n=n,grid=valueStep)[:2] + if y2 - y1 < requiredRange: + ym = (y1+y2)*0.5 + y1 = min(ym-requiredRange*0.5,y_min) + y2 = max(ym+requiredRange*0.5,y_max) + if y_min>=100 and y1<100: + y2 = y2 + 100 - y1 + y1 = 100 + elif y_min>=0 and y1<0: + y2 = y2 - y1 + y1 = 0 + self._valueMin, self._valueMax = y1, y2 + + T, L = ticks(self._valueMin, self._valueMax, split=1, n=n, percent=self.leftAxisPercent,grid=valueStep) + abf = self.avoidBoundFrac + if abf: + i1 = (T[1]-T[0]) + if type(abf) not in (TupleType,ListType): + i0 = i1 = i1*abf + else: + i0 = i1*abf[0] + i1 = i1*abf[1] + _n = getattr(self,'_cValueMin',T[0]) + _x = getattr(self,'_cValueMax',T[-1]) + if _n - T[0] < i0: self._valueMin = self._valueMin - i0 + if T[-1]-_x < i1: self._valueMax = self._valueMax + i1 + T, L = ticks(self._valueMin, self._valueMax, split=1, n=n, percent=self.leftAxisPercent,grid=valueStep) + + self._valueMin = T[0] + self._valueMax = T[-1] + self._tickValues = self.valueSteps = T + if self.labelTextFormat is None: + self._labelTextFormat = L + else: + self._labelTextFormat = self.labelTextFormat + + if abs(self._valueMin-100)<1e-6: + self._calcValueStep() + vMax, vMin = self._valueMax, self._valueMin + m = max(self.leftAxisOrigShiftIPC*self._valueStep, + (vMax-vMin)*self.leftAxisOrigShiftMin/self._length) + self._valueMin = self._valueMin - m + + if self.leftAxisSkipLL0: + if type(self.leftAxisSkipLL0) in (ListType,TupleType): + for x in self.leftAxisSkipLL0: + try: + L[x] = '' + except IndexError: + pass + L[0] = '' + +# Sample functions. +def sample0a(): + "Sample drawing with one xcat axis and two buckets." + + drawing = Drawing(400, 200) + + data = [(10, 20)] + + xAxis = XCategoryAxis() + xAxis.setPosition(75, 75, 300) + xAxis.configure(data) + xAxis.categoryNames = ['Ying', 'Yang'] + xAxis.labels.boxAnchor = 'n' + + drawing.add(xAxis) + + return drawing + + +def sample0b(): + "Sample drawing with one xcat axis and one bucket only." + + drawing = Drawing(400, 200) + + data = [(10,)] + + xAxis = XCategoryAxis() + xAxis.setPosition(75, 75, 300) + xAxis.configure(data) + xAxis.categoryNames = ['Ying'] + xAxis.labels.boxAnchor = 'n' + + drawing.add(xAxis) + + return drawing + + +def sample1(): + "Sample drawing containing two unconnected axes." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + xAxis = XCategoryAxis() + xAxis.setPosition(75, 75, 300) + xAxis.configure(data) + xAxis.categoryNames = ['Beer','Wine','Meat','Cannelloni'] + xAxis.labels.boxAnchor = 'n' + xAxis.labels[3].dy = -15 + xAxis.labels[3].angle = 30 + xAxis.labels[3].fontName = 'Times-Bold' + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +##def sample2a(): +## "Make sample drawing with two axes, x connected at top of y." +## +## drawing = Drawing(400, 200) +## +## data = [(10, 20, 30, 42)] +## +## yAxis = YValueAxis() +## yAxis.setPosition(50, 50, 125) +## yAxis.configure(data) +## +## xAxis = XCategoryAxis() +## xAxis._length = 300 +## xAxis.configure(data) +## xAxis.joinToAxis(yAxis, mode='top') +## xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] +## xAxis.labels.boxAnchor = 'n' +## +## drawing.add(xAxis) +## drawing.add(yAxis) +## +## return drawing +## +## +##def sample2b(): +## "Make two axes, x connected at bottom of y." +## +## drawing = Drawing(400, 200) +## +## data = [(10, 20, 30, 42)] +## +## yAxis = YValueAxis() +## yAxis.setPosition(50, 50, 125) +## yAxis.configure(data) +## +## xAxis = XCategoryAxis() +## xAxis._length = 300 +## xAxis.configure(data) +## xAxis.joinToAxis(yAxis, mode='bottom') +## xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] +## xAxis.labels.boxAnchor = 'n' +## +## drawing.add(xAxis) +## drawing.add(yAxis) +## +## return drawing +## +## +##def sample2c(): +## "Make two axes, x connected at fixed value (in points) of y." +## +## drawing = Drawing(400, 200) +## +## data = [(10, 20, 30, 42)] +## +## yAxis = YValueAxis() +## yAxis.setPosition(50, 50, 125) +## yAxis.configure(data) +## +## xAxis = XCategoryAxis() +## xAxis._length = 300 +## xAxis.configure(data) +## xAxis.joinToAxis(yAxis, mode='points', pos=100) +## xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] +## xAxis.labels.boxAnchor = 'n' +## +## drawing.add(xAxis) +## drawing.add(yAxis) +## +## return drawing +## +## +##def sample2d(): +## "Make two axes, x connected at fixed value (of y-axes) of y." +## +## drawing = Drawing(400, 200) +## +## data = [(10, 20, 30, 42)] +## +## yAxis = YValueAxis() +## yAxis.setPosition(50, 50, 125) +## yAxis.configure(data) +## +## xAxis = XCategoryAxis() +## xAxis._length = 300 +## xAxis.configure(data) +## xAxis.joinToAxis(yAxis, mode='value', pos=20) +## xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] +## xAxis.labels.boxAnchor = 'n' +## +## drawing.add(xAxis) +## drawing.add(yAxis) +## +## return drawing +## +## +##def sample3a(): +## "Make sample drawing with two axes, y connected at left of x." +## +## drawing = Drawing(400, 200) +## +## data = [(10, 20, 30, 42)] +## +## xAxis = XCategoryAxis() +## xAxis._length = 300 +## xAxis.configure(data) +## xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] +## xAxis.labels.boxAnchor = 'n' +## +## yAxis = YValueAxis() +## yAxis.setPosition(50, 50, 125) +## yAxis.configure(data) +## yAxis.joinToAxis(xAxis, mode='left') +## +## drawing.add(xAxis) +## drawing.add(yAxis) +## +## return drawing +## +## +##def sample3b(): +## "Make sample drawing with two axes, y connected at right of x." +## +## drawing = Drawing(400, 200) +## +## data = [(10, 20, 30, 42)] +## +## xAxis = XCategoryAxis() +## xAxis._length = 300 +## xAxis.configure(data) +## xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] +## xAxis.labels.boxAnchor = 'n' +## +## yAxis = YValueAxis() +## yAxis.setPosition(50, 50, 125) +## yAxis.configure(data) +## yAxis.joinToAxis(xAxis, mode='right') +## +## drawing.add(xAxis) +## drawing.add(yAxis) +## +## return drawing +## +## +##def sample3c(): +## "Make two axes, y connected at fixed value (in points) of x." +## +## drawing = Drawing(400, 200) +## +## data = [(10, 20, 30, 42)] +## +## yAxis = YValueAxis() +## yAxis.setPosition(50, 50, 125) +## yAxis.configure(data) +## +## xAxis = XValueAxis() +## xAxis._length = 300 +## xAxis.configure(data) +## xAxis.joinToAxis(yAxis, mode='points', pos=100) +## +## drawing.add(xAxis) +## drawing.add(yAxis) +## +## return drawing + + +def sample4a(): + "Sample drawing, xvalue/yvalue axes, y connected at 100 pts to x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + + xAxis = XValueAxis() + xAxis._length = 300 + xAxis.joinAxis = yAxis + xAxis.joinAxisMode = 'points' + xAxis.joinAxisPos = 100 + xAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample4b(): + "Sample drawing, xvalue/yvalue axes, y connected at value 35 of x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + + xAxis = XValueAxis() + xAxis._length = 300 + xAxis.joinAxis = yAxis + xAxis.joinAxisMode = 'value' + xAxis.joinAxisPos = 35 + xAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample4c(): + "Sample drawing, xvalue/yvalue axes, y connected to bottom of x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + + xAxis = XValueAxis() + xAxis._length = 300 + xAxis.joinAxis = yAxis + xAxis.joinAxisMode = 'bottom' + xAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample4c1(): + "xvalue/yvalue axes, without drawing axis lines/ticks." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + yAxis.visibleAxis = 0 + yAxis.visibleTicks = 0 + + xAxis = XValueAxis() + xAxis._length = 300 + xAxis.joinAxis = yAxis + xAxis.joinAxisMode = 'bottom' + xAxis.configure(data) + xAxis.visibleAxis = 0 + xAxis.visibleTicks = 0 + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample4d(): + "Sample drawing, xvalue/yvalue axes, y connected to top of x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + + xAxis = XValueAxis() + xAxis._length = 300 + xAxis.joinAxis = yAxis + xAxis.joinAxisMode = 'top' + xAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample5a(): + "Sample drawing, xvalue/yvalue axes, y connected at 100 pts to x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + xAxis = XValueAxis() + xAxis.setPosition(50, 50, 300) + xAxis.configure(data) + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.joinAxis = xAxis + yAxis.joinAxisMode = 'points' + yAxis.joinAxisPos = 100 + yAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample5b(): + "Sample drawing, xvalue/yvalue axes, y connected at value 35 of x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + xAxis = XValueAxis() + xAxis.setPosition(50, 50, 300) + xAxis.configure(data) + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.joinAxis = xAxis + yAxis.joinAxisMode = 'value' + yAxis.joinAxisPos = 35 + yAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample5c(): + "Sample drawing, xvalue/yvalue axes, y connected at right of x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + xAxis = XValueAxis() + xAxis.setPosition(50, 50, 300) + xAxis.configure(data) + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.joinAxis = xAxis + yAxis.joinAxisMode = 'right' + yAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample5d(): + "Sample drawing, xvalue/yvalue axes, y connected at left of x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + xAxis = XValueAxis() + xAxis.setPosition(50, 50, 300) + xAxis.configure(data) + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.joinAxis = xAxis + yAxis.joinAxisMode = 'left' + yAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample6a(): + "Sample drawing, xcat/yvalue axes, x connected at top of y." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + + xAxis = XCategoryAxis() + xAxis._length = 300 + xAxis.configure(data) + xAxis.joinAxis = yAxis + xAxis.joinAxisMode = 'top' + xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] + xAxis.labels.boxAnchor = 'n' + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample6b(): + "Sample drawing, xcat/yvalue axes, x connected at bottom of y." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + + xAxis = XCategoryAxis() + xAxis._length = 300 + xAxis.configure(data) + xAxis.joinAxis = yAxis + xAxis.joinAxisMode = 'bottom' + xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] + xAxis.labels.boxAnchor = 'n' + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample6c(): + "Sample drawing, xcat/yvalue axes, x connected at 100 pts to y." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + + xAxis = XCategoryAxis() + xAxis._length = 300 + xAxis.configure(data) + xAxis.joinAxis = yAxis + xAxis.joinAxisMode = 'points' + xAxis.joinAxisPos = 100 + xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] + xAxis.labels.boxAnchor = 'n' + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample6d(): + "Sample drawing, xcat/yvalue axes, x connected at value 20 of y." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + + xAxis = XCategoryAxis() + xAxis._length = 300 + xAxis.configure(data) + xAxis.joinAxis = yAxis + xAxis.joinAxisMode = 'value' + xAxis.joinAxisPos = 20 + xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] + xAxis.labels.boxAnchor = 'n' + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample7a(): + "Sample drawing, xvalue/ycat axes, y connected at right of x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + xAxis = XValueAxis() + xAxis._length = 300 + xAxis.configure(data) + + yAxis = YCategoryAxis() + yAxis.setPosition(50, 50, 125) + yAxis.joinAxis = xAxis + yAxis.joinAxisMode = 'right' + yAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] + yAxis.labels.boxAnchor = 'e' + yAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample7b(): + "Sample drawing, xvalue/ycat axes, y connected at left of x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + xAxis = XValueAxis() + xAxis._length = 300 + xAxis.configure(data) + + yAxis = YCategoryAxis() + yAxis.setPosition(50, 50, 125) + yAxis.joinAxis = xAxis + yAxis.joinAxisMode = 'left' + yAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] + yAxis.labels.boxAnchor = 'e' + yAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample7c(): + "Sample drawing, xvalue/ycat axes, y connected at value 30 of x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + xAxis = XValueAxis() + xAxis._length = 300 + xAxis.configure(data) + + yAxis = YCategoryAxis() + yAxis.setPosition(50, 50, 125) + yAxis.joinAxis = xAxis + yAxis.joinAxisMode = 'value' + yAxis.joinAxisPos = 30 + yAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] + yAxis.labels.boxAnchor = 'e' + yAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample7d(): + "Sample drawing, xvalue/ycat axes, y connected at 200 pts to x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + xAxis = XValueAxis() + xAxis._length = 300 + xAxis.configure(data) + + yAxis = YCategoryAxis() + yAxis.setPosition(50, 50, 125) + yAxis.joinAxis = xAxis + yAxis.joinAxisMode = 'points' + yAxis.joinAxisPos = 200 + yAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] + yAxis.labels.boxAnchor = 'e' + yAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing diff --git a/bin/reportlab/graphics/charts/barcharts.py b/bin/reportlab/graphics/charts/barcharts.py new file mode 100644 index 00000000000..0ca1ae3ed57 --- /dev/null +++ b/bin/reportlab/graphics/charts/barcharts.py @@ -0,0 +1,1972 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/barcharts.py +"""This module defines a variety of Bar Chart components. + +The basic flavors are Side-by-side, available in horizontal and +vertical versions. + +Stacked and percentile bar charts to follow... +""" +__version__=''' $Id: barcharts.py 2647 2005-07-26 13:47:51Z rgbecker $ ''' + +import string, copy +from types import FunctionType, StringType + +from reportlab.lib import colors +from reportlab.lib.validators import isNumber, isColor, isColorOrNone, isString,\ + isListOfStrings, SequenceOf, isBoolean, isNoneOrShape, isStringOrNone,\ + NoneOr +from reportlab.graphics.widgets.markers import uSymbol2Symbol, isSymbol +from reportlab.lib.formatters import Formatter +from reportlab.lib.attrmap import AttrMap, AttrMapValue +from reportlab.pdfbase.pdfmetrics import stringWidth +from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder +from reportlab.graphics.shapes import Line, Rect, Group, Drawing, NotImplementedError +from reportlab.graphics.charts.axes import XCategoryAxis, YValueAxis, YCategoryAxis, XValueAxis +from reportlab.graphics.charts.textlabels import BarChartLabel, NA_Label, NoneOrInstanceOfNA_Label +from reportlab.graphics.charts.areas import PlotArea + +class BarChartProperties(PropHolder): + _attrMap = AttrMap( + strokeColor = AttrMapValue(isColorOrNone, desc='Color of the bar border.'), + fillColor = AttrMapValue(isColorOrNone, desc='Color of the bar interior area.'), + strokeWidth = AttrMapValue(isNumber, desc='Width of the bar border.'), + symbol = AttrMapValue(None, desc='A widget to be used instead of a normal bar.'), + name = AttrMapValue(isString, desc='Text to be associated with a bar (eg seriesname)'), + swatchMarker = AttrMapValue(NoneOr(isSymbol), desc="None or makeMarker('Diamond') ..."), + ) + + def __init__(self): + self.strokeColor = None + self.fillColor = colors.blue + self.strokeWidth = 0.5 + self.symbol = None + +# Bar chart classes. +class BarChart(PlotArea): + "Abstract base class, unusable by itself." + + _attrMap = AttrMap(BASE=PlotArea, + useAbsolute = AttrMapValue(isNumber, desc='Flag to use absolute spacing values.'), + barWidth = AttrMapValue(isNumber, desc='The width of an individual bar.'), + groupSpacing = AttrMapValue(isNumber, desc='Width between groups of bars.'), + barSpacing = AttrMapValue(isNumber, desc='Width between individual bars.'), + bars = AttrMapValue(None, desc='Handle of the individual bars.'), + valueAxis = AttrMapValue(None, desc='Handle of the value axis.'), + categoryAxis = AttrMapValue(None, desc='Handle of the category axis.'), + data = AttrMapValue(None, desc='Data to be plotted, list of (lists of) numbers.'), + barLabels = AttrMapValue(None, desc='Handle to the list of bar labels.'), + barLabelFormat = AttrMapValue(None, desc='Formatting string or function used for bar labels.'), + barLabelCallOut = AttrMapValue(None, desc='Callout function(label)\nlabel._callOutInfo = (self,g,rowNo,colNo,x,y,width,height,x00,y00,x0,y0)'), + barLabelArray = AttrMapValue(None, desc='explicit array of bar label values, must match size of data if present.'), + reversePlotOrder = AttrMapValue(isBoolean, desc='If true, reverse common category plot order.'), + naLabel = AttrMapValue(NoneOrInstanceOfNA_Label, desc='Label to use for N/A values.'), + annotations = AttrMapValue(None, desc='list of callables, will be called with self, xscale, yscale.'), + ) + + def makeSwatchSample(self, rowNo, x, y, width, height): + baseStyle = self.bars + 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(style, 'strokeWidth',None)) + swatchMarker = getattr(style, 'swatchMarker', getattr(baseStyle, 'swatchMarker',None)) + if swatchMarker: + return uSymbol2Symbol(swatchMarker,x+width/2.,y+height/2.,fillColor) + return Rect(x,y,width,height,strokeWidth=strokeWidth,strokeColor=strokeColor, + strokeDashArray=strokeDashArray,fillColor=fillColor) + + def getSeriesName(self,i,default=None): + '''return series name i or default''' + return getattr(self.bars[i],'name',default) + + def __init__(self): + assert self.__class__.__name__ not in ('BarChart','BarChart3D'), 'Abstract Class %s Instantiated' % self.__class__.__name__ + + if self._flipXY: + self.categoryAxis = YCategoryAxis() + self.valueAxis = XValueAxis() + else: + self.categoryAxis = XCategoryAxis() + self.valueAxis = YValueAxis() + + PlotArea.__init__(self) + self.barSpacing = 0 + self.reversePlotOrder = 0 + + + # this defines two series of 3 points. Just an example. + self.data = [(100,110,120,130), + (70, 80, 85, 90)] + + # control bar spacing. is useAbsolute = 1 then + # the next parameters are in points; otherwise + # they are 'proportions' and are normalized to + # fit the available space. Half a barSpacing + # is allocated at the beginning and end of the + # chart. + self.useAbsolute = 0 #- not done yet + self.barWidth = 10 + self.groupSpacing = 5 + self.barSpacing = 0 + + self.barLabels = TypedPropertyCollection(BarChartLabel) + self.barLabels.boxAnchor = 'c' + self.barLabels.textAnchor = 'middle' + self.barLabelFormat = None + self.barLabelArray = None + # this says whether the origin is inside or outside + # the bar - +10 means put the origin ten points + # above the tip of the bar if value > 0, or ten + # points inside if bar value < 0. This is different + # to label dx/dy which are not dependent on the + # sign of the data. + self.barLabels.nudge = 0 + + # if you have multiple series, by default they butt + # together. + + # we really need some well-designed default lists of + # colors e.g. from Tufte. These will be used in a + # cycle to set the fill color of each series. + self.bars = TypedPropertyCollection(BarChartProperties) +## self.bars.symbol = None + self.bars.strokeWidth = 1 + self.bars.strokeColor = colors.black + + self.bars[0].fillColor = colors.red + self.bars[1].fillColor = colors.green + self.bars[2].fillColor = colors.blue + self.naLabel = None#NA_Label() + + + def demo(self): + """Shows basic use of a bar chart""" + if self.__class__.__name__=='BarChart': + raise NotImplementedError, 'Abstract Class BarChart has no demo' + drawing = Drawing(200, 100) + bc = self.__class__() + drawing.add(bc) + return drawing + + def _getConfigureData(self): + cA = self.categoryAxis + data = self.data + if cA.style not in ('parallel','parallel_3d'): + _data = data + data = max(map(len,_data))*[0] + for d in _data: + for i in xrange(len(d)): + data[i] = data[i] + (d[i] or 0) + data = list(_data) + [data] + self._configureData = data + + def _getMinMax(self): + '''Attempt to return the data range''' + self._getConfigureData() + self.valueAxis._setRange(self._configureData) + return self.valueAxis._valueMin, self.valueAxis._valueMax + + def _drawBegin(self,org,length): + '''Position and configure value axis, return crossing value''' + vA = self.valueAxis + vA.setPosition(self.x, self.y, length) + self._getConfigureData() + vA.configure(self._configureData) + + # if zero is in chart, put the other axis there, otherwise use low + crossesAt = vA.scale(0) + if crossesAt > org+length or crossesAt=0 and 1 or -1)*nudge, y + 0.5*height + else: + value = height + if anti: value = 0 + return x + 0.5*width, y + value + (height>=0 and 1 or -1)*nudge + + def _addBarLabel(self, g, rowNo, colNo, x, y, width, height): + text = self._getLabelText(rowNo,colNo) + if text: + self._addLabel(text, self.barLabels[(rowNo, colNo)], g, rowNo, colNo, x, y, width, height) + + def _addNABarLabel(self, g, rowNo, colNo, x, y, width, height): + na = self.naLabel + if na and na.text: + na = copy.copy(na) + v = self.valueAxis._valueMax<=0 and -1e-8 or 1e-8 + if width is None: width = v + if height is None: height = v + self._addLabel(na.text, na, g, rowNo, colNo, x, y, width, height) + + def _addLabel(self, text, label, g, rowNo, colNo, x, y, width, height): + if label.visible: + labelWidth = stringWidth(text, label.fontName, label.fontSize) + x0, y0 = self._labelXY(label,x,y,width,height) + flipXY = self._flipXY + if flipXY: + pm = width + else: + pm = height + label._pmv = pm #the plus minus val + fixedEnd = getattr(label,'fixedEnd', None) + if fixedEnd is not None: + v = fixedEnd._getValue(self,pm) + x00, y00 = x0, y0 + if flipXY: + x0 = v + else: + y0 = v + else: + if flipXY: + x00 = x0 + y00 = y+height/2.0 + else: + x00 = x+width/2.0 + y00 = y0 + fixedStart = getattr(label,'fixedStart', None) + if fixedStart is not None: + v = fixedStart._getValue(self,pm) + if flipXY: + x00 = v + else: + y00 = v + + if pm<0: + if flipXY: + dx = -2*label.dx + dy = 0 + else: + dy = -2*label.dy + dx = 0 + else: + dy = dx = 0 + label.setOrigin(x0+dx, y0+dy) + label.setText(text) + sC, sW = label.lineStrokeColor, label.lineStrokeWidth + if sC and sW: g.insert(0,Line(x00,y00,x0,y0, strokeColor=sC, strokeWidth=sW)) + g.add(label) + alx = getattr(self,'barLabelCallOut',None) + if alx: + label._callOutInfo = (self,g,rowNo,colNo,x,y,width,height,x00,y00,x0,y0) + alx(label) + del label._callOutInfo + + def _makeBar(self,g,x,y,width,height,rowNo,style): + r = Rect(x, y, width, height) + r.strokeWidth = style.strokeWidth + r.fillColor = style.fillColor + r.strokeColor = style.strokeColor + g.add(r) + + def _makeBars(self,g,lg): + lenData = len(self.data) + bars = self.bars + for rowNo in range(lenData): + row = self._barPositions[rowNo] + styleCount = len(bars) + styleIdx = rowNo % styleCount + rowStyle = bars[styleIdx] + for colNo in range(len(row)): + barPos = row[colNo] + style = bars.has_key((styleIdx,colNo)) and bars[(styleIdx,colNo)] or rowStyle + (x, y, width, height) = barPos + if None in (width,height): + self._addNABarLabel(lg,rowNo,colNo,x,y,width,height) + continue + + # Draw a rectangular symbol for each data item, + # or a normal colored rectangle. + symbol = None + if hasattr(style, 'symbol'): + symbol = copy.deepcopy(style.symbol) + elif hasattr(self.bars, 'symbol'): + symbol = self.bars.symbol + + if symbol: + symbol.x = x + symbol.y = y + symbol.width = width + symbol.height = height + g.add(symbol) + elif abs(width)>1e-7 and abs(height)>=1e-7 and (style.fillColor is not None or style.strokeColor is not None): + self._makeBar(g,x,y,width,height,rowNo,style) + + self._addBarLabel(lg,rowNo,colNo,x,y,width,height) + + def makeBars(self): + g = Group() + lg = Group() + self._makeBars(g,lg) + g.add(lg) + return g + + def _desiredCategoryAxisLength(self): + '''for dynamically computing the desired category axis length''' + style = self.categoryAxis.style + data = self.data + n = len(data) + m = max(map(len,data)) + if style=='parallel': + groupWidth = (n-1)*self.barSpacing+n*self.barWidth + else: + groupWidth = self.barWidth + return m*(self.groupSpacing+groupWidth) + + def draw(self): + cA, vA = self.categoryAxis, self.valueAxis + if vA: ovAjA, vA.joinAxis = vA.joinAxis, cA + if cA: ocAjA, cA.joinAxis = cA.joinAxis, vA + if self._flipXY: + cA.setPosition(self._drawBegin(self.x,self.width), self.y, self.height) + else: + cA.setPosition(self.x, self._drawBegin(self.y,self.height), self.width) + return self._drawFinish() + +class VerticalBarChart(BarChart): + "Vertical bar chart with multiple side-by-side bars." + _flipXY = 0 + +class HorizontalBarChart(BarChart): + "Horizontal bar chart with multiple side-by-side bars." + _flipXY = 1 + +class _FakeGroup: + def __init__(self, cmp=None): + self._data = [] + self._cmp = cmp + + def add(self,what): + self._data.append(what) + + def value(self): + return self._data + + def sort(self): + self._data.sort(self._cmp) + +class BarChart3D(BarChart): + _attrMap = AttrMap(BASE=BarChart, + 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 = None + zSpace = None + + def calcBarPositions(self): + BarChart.calcBarPositions(self) + seriesCount = self._seriesCount + zDepth = self.zDepth + if zDepth is None: zDepth = self.barWidth + zSpace = self.zSpace + if zSpace is None: zSpace = self.barSpacing + if self.categoryAxis.style=='parallel_3d': + _3d_depth = seriesCount*zDepth+(seriesCount+1)*zSpace + else: + _3d_depth = zDepth + 2*zSpace + _3d_depth *= self._normFactor + self._3d_dx = self.theta_x*_3d_depth + self._3d_dy = self.theta_y*_3d_depth + + def _calc_z0(self,rowNo): + zDepth = self.zDepth + if zDepth is None: zDepth = self.barWidth + zSpace = self.zSpace + if zSpace is None: zSpace = self.barSpacing + if self.categoryAxis.style=='parallel_3d': + z0 = self._normFactor*(rowNo*(zDepth+zSpace)+zSpace) + else: + z0 = self._normFactor*zSpace + return z0 + + def _makeBar(self,g,x,y,width,height,rowNo,style): + zDepth = self.zDepth + if zDepth is None: zDepth = self.barWidth + zSpace = self.zSpace + if zSpace is None: zSpace = self.barSpacing + z0 = self._calc_z0(rowNo) + z1 = z0 + zDepth*self._normFactor + if width<0: + x += width + width = -width + x += z0*self.theta_x + y += z0*self.theta_y + if self._flipXY: + y += zSpace + else: + x += zSpace + g.add((0,z0,z1,x,y,width,height,rowNo,style)) + + def _addBarLabel(self, g, rowNo, colNo, x, y, width, height): + z0 = self._calc_z0(rowNo) + zSpace = self.zSpace + if zSpace is None: zSpace = self.barSpacing + z1 = z0 + x += z0*self.theta_x + y += z0*self.theta_y + if self._flipXY: + y += zSpace + else: + x += zSpace + g.add((1,z0,z1,x,y,width,height,rowNo,colNo)) + + def makeBars(self): + from utils3d import _draw_3d_bar + fg = _FakeGroup(cmp=self._cmpZ) + self._makeBars(fg,fg) + fg.sort() + g = Group() + theta_x = self.theta_x + theta_y = self.theta_y + for t in fg.value(): + if t[0]==1: + z0,z1,x,y,width,height,rowNo,colNo = t[1:] + BarChart._addBarLabel(self,g,rowNo,colNo,x,y,width,height) + elif t[0]==0: + z0,z1,x,y,width,height,rowNo,style = t[1:] + dz = z1 - z0 + _draw_3d_bar(g, x, x+width, y, y+height, dz*theta_x, dz*theta_y, + fillColor=style.fillColor, fillColorShaded=None, + strokeColor=style.strokeColor, strokeWidth=style.strokeWidth, + shading=0.45) + return g + +class VerticalBarChart3D(BarChart3D,VerticalBarChart): + _cmpZ=lambda self,a,b:cmp((-a[1],a[3],a[0],-a[4]),(-b[1],b[3],b[0],-b[4])) + +class HorizontalBarChart3D(BarChart3D,HorizontalBarChart): + _cmpZ = lambda self,a,b: cmp((-a[1],a[4],a[0],-a[3]),(-b[1],b[4],b[0],-b[3])) #t, z0, z1, x, y = a[:5] + +# Vertical samples. +def sampleV0a(): + "A slightly pathologic bar chart with only TWO data items." + + drawing = Drawing(400, 200) + + data = [(13, 20)] + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'ne' + bc.categoryAxis.labels.dx = 8 + bc.categoryAxis.labels.dy = -2 + bc.categoryAxis.labels.angle = 30 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleV0b(): + "A pathologic bar chart with only ONE data item." + + drawing = Drawing(400, 200) + + data = [(42,)] + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 50 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'ne' + bc.categoryAxis.labels.dx = 8 + bc.categoryAxis.labels.dy = -2 + bc.categoryAxis.labels.angle = 30 + bc.categoryAxis.categoryNames = ['Jan-99'] + + drawing.add(bc) + + return drawing + + +def sampleV0c(): + "A really pathologic bar chart with NO data items at all!" + + drawing = Drawing(400, 200) + + data = [()] + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'ne' + bc.categoryAxis.labels.dx = 8 + bc.categoryAxis.labels.dy = -2 + bc.categoryAxis.categoryNames = [] + + drawing.add(bc) + + return drawing + + +def sampleV1(): + "Sample of multi-series bar chart." + + drawing = Drawing(400, 200) + + data = [ + (13, 5, 20, 22, 37, 45, 19, 4), + (14, 6, 21, 23, 38, 46, 20, 5) + ] + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'ne' + bc.categoryAxis.labels.dx = 8 + bc.categoryAxis.labels.dy = -2 + bc.categoryAxis.labels.angle = 30 + + catNames = string.split('Jan Feb Mar Apr May Jun Jul Aug', ' ') + catNames = map(lambda n:n+'-99', catNames) + bc.categoryAxis.categoryNames = catNames + drawing.add(bc) + + return drawing + + +def sampleV2a(): + "Sample of multi-series bar chart." + + data = [(2.4, -5.7, 2, 5, 9.2), + (0.6, -4.9, -3, 4, 6.8) + ] + + labels = ("Q3 2000", "Year to Date", "12 months", + "Annualised\n3 years", "Since 07.10.99") + + drawing = Drawing(400, 200) + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 120 + bc.width = 300 + bc.data = data + + bc.barSpacing = 0 + bc.groupSpacing = 10 + bc.barWidth = 10 + + bc.valueAxis.valueMin = -15 + bc.valueAxis.valueMax = +15 + bc.valueAxis.valueStep = 5 + bc.valueAxis.labels.fontName = 'Helvetica' + bc.valueAxis.labels.fontSize = 8 + bc.valueAxis.labels.boxAnchor = 'n' # irrelevant (becomes 'c') + bc.valueAxis.labels.textAnchor = 'middle' + + bc.categoryAxis.categoryNames = labels + bc.categoryAxis.labels.fontName = 'Helvetica' + bc.categoryAxis.labels.fontSize = 8 + bc.categoryAxis.labels.dy = -60 + + drawing.add(bc) + + return drawing + + +def sampleV2b(): + "Sample of multi-series bar chart." + + data = [(2.4, -5.7, 2, 5, 9.2), + (0.6, -4.9, -3, 4, 6.8) + ] + + labels = ("Q3 2000", "Year to Date", "12 months", + "Annualised\n3 years", "Since 07.10.99") + + drawing = Drawing(400, 200) + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 120 + bc.width = 300 + bc.data = data + + bc.barSpacing = 5 + bc.groupSpacing = 10 + bc.barWidth = 10 + + bc.valueAxis.valueMin = -15 + bc.valueAxis.valueMax = +15 + bc.valueAxis.valueStep = 5 + bc.valueAxis.labels.fontName = 'Helvetica' + bc.valueAxis.labels.fontSize = 8 + bc.valueAxis.labels.boxAnchor = 'n' # irrelevant (becomes 'c') + bc.valueAxis.labels.textAnchor = 'middle' + + bc.categoryAxis.categoryNames = labels + bc.categoryAxis.labels.fontName = 'Helvetica' + bc.categoryAxis.labels.fontSize = 8 + bc.categoryAxis.labels.dy = -60 + + drawing.add(bc) + + return drawing + + +def sampleV2c(): + "Sample of multi-series bar chart." + + data = [(2.4, -5.7, 2, 5, 9.99), + (0.6, -4.9, -3, 4, 9.99) + ] + + labels = ("Q3 2000", "Year to Date", "12 months", + "Annualised\n3 years", "Since 07.10.99") + + drawing = Drawing(400, 200) + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 120 + bc.width = 300 + bc.data = data + + bc.barSpacing = 2 + bc.groupSpacing = 10 + bc.barWidth = 10 + + bc.valueAxis.valueMin = -15 + bc.valueAxis.valueMax = +15 + bc.valueAxis.valueStep = 5 + bc.valueAxis.labels.fontName = 'Helvetica' + bc.valueAxis.labels.fontSize = 8 + + bc.categoryAxis.categoryNames = labels + bc.categoryAxis.labels.fontName = 'Helvetica' + bc.categoryAxis.labels.fontSize = 8 + bc.valueAxis.labels.boxAnchor = 'n' + bc.valueAxis.labels.textAnchor = 'middle' + bc.categoryAxis.labels.dy = -60 + + bc.barLabels.nudge = 10 + + bc.barLabelFormat = '%0.2f' + bc.barLabels.dx = 0 + bc.barLabels.dy = 0 + bc.barLabels.boxAnchor = 'n' # irrelevant (becomes 'c') + bc.barLabels.fontName = 'Helvetica' + bc.barLabels.fontSize = 6 + + drawing.add(bc) + + return drawing + + +def sampleV3(): + "Faked horizontal bar chart using a vertical real one (deprecated)." + + names = ("UK Equities", "US Equities", "European Equities", "Japanese Equities", + "Pacific (ex Japan) Equities", "Emerging Markets Equities", + "UK Bonds", "Overseas Bonds", "UK Index-Linked", "Cash") + + series1 = (-1.5, 0.3, 0.5, 1.0, 0.8, 0.7, 0.4, 0.1, 1.0, 0.3) + series2 = (0.0, 0.33, 0.55, 1.1, 0.88, 0.77, 0.44, 0.11, 1.10, 0.33) + + assert len(names) == len(series1), "bad data" + assert len(names) == len(series2), "bad data" + + drawing = Drawing(400, 200) + + bc = VerticalBarChart() + bc.x = 0 + bc.y = 0 + bc.height = 100 + bc.width = 150 + bc.data = (series1,) + bc.bars.fillColor = colors.green + + bc.barLabelFormat = '%0.2f' + bc.barLabels.dx = 0 + bc.barLabels.dy = 0 + bc.barLabels.boxAnchor = 'w' # irrelevant (becomes 'c') + bc.barLabels.angle = 90 + bc.barLabels.fontName = 'Helvetica' + bc.barLabels.fontSize = 6 + bc.barLabels.nudge = 10 + + bc.valueAxis.visible = 0 + bc.valueAxis.valueMin = -2 + bc.valueAxis.valueMax = +2 + bc.valueAxis.valueStep = 1 + + bc.categoryAxis.tickUp = 0 + bc.categoryAxis.tickDown = 0 + bc.categoryAxis.categoryNames = names + bc.categoryAxis.labels.angle = 90 + bc.categoryAxis.labels.boxAnchor = 'w' + bc.categoryAxis.labels.dx = 0 + bc.categoryAxis.labels.dy = -125 + bc.categoryAxis.labels.fontName = 'Helvetica' + bc.categoryAxis.labels.fontSize = 6 + + g = Group(bc) + g.translate(100, 175) + g.rotate(-90) + + drawing.add(g) + + return drawing + + +def sampleV4a(): + "A bar chart showing value axis region starting at *exactly* zero." + + drawing = Drawing(400, 200) + + data = [(13, 20)] + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleV4b(): + "A bar chart showing value axis region starting *below* zero." + + drawing = Drawing(400, 200) + + data = [(13, 20)] + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = -10 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleV4c(): + "A bar chart showing value axis region staring *above* zero." + + drawing = Drawing(400, 200) + + data = [(13, 20)] + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 10 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleV4d(): + "A bar chart showing value axis region entirely *below* zero." + + drawing = Drawing(400, 200) + + data = [(-13, -20)] + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = -30 + bc.valueAxis.valueMax = -10 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +### + +##dataSample5 = [(10, 20), (20, 30), (30, 40), (40, 50), (50, 60)] +##dataSample5 = [(10, 60), (20, 50), (30, 40), (40, 30), (50, 20)] +dataSample5 = [(10, 60), (20, 50), (30, 40), (40, 30)] + +def sampleV5a(): + "A simple bar chart with no expressed spacing attributes." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleV5b(): + "A simple bar chart with proportional spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 0 + bc.barWidth = 40 + bc.groupSpacing = 20 + bc.barSpacing = 10 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleV5c1(): + "Make sampe simple bar chart but with absolute spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 1 + bc.barWidth = 40 + bc.groupSpacing = 0 + bc.barSpacing = 0 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleV5c2(): + "Make sampe simple bar chart but with absolute spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 1 + bc.barWidth = 40 + bc.groupSpacing = 20 + bc.barSpacing = 0 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleV5c3(): + "Make sampe simple bar chart but with absolute spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 1 + bc.barWidth = 40 + bc.groupSpacing = 0 + bc.barSpacing = 10 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleV5c4(): + "Make sampe simple bar chart but with absolute spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 1 + bc.barWidth = 40 + bc.groupSpacing = 20 + bc.barSpacing = 10 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +# Horizontal samples + +def sampleH0a(): + "Make a slightly pathologic bar chart with only TWO data items." + + drawing = Drawing(400, 200) + + data = [(13, 20)] + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'se' + bc.categoryAxis.labels.angle = 30 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleH0b(): + "Make a pathologic bar chart with only ONE data item." + + drawing = Drawing(400, 200) + + data = [(42,)] + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 50 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'se' + bc.categoryAxis.labels.angle = 30 + bc.categoryAxis.categoryNames = ['Jan-99'] + + drawing.add(bc) + + return drawing + + +def sampleH0c(): + "Make a really pathologic bar chart with NO data items at all!" + + drawing = Drawing(400, 200) + + data = [()] + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'se' + bc.categoryAxis.labels.angle = 30 + bc.categoryAxis.categoryNames = [] + + drawing.add(bc) + + return drawing + + +def sampleH1(): + "Sample of multi-series bar chart." + + drawing = Drawing(400, 200) + + data = [ + (13, 5, 20, 22, 37, 45, 19, 4), + (14, 6, 21, 23, 38, 46, 20, 5) + ] + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + catNames = string.split('Jan Feb Mar Apr May Jun Jul Aug', ' ') + catNames = map(lambda n:n+'-99', catNames) + bc.categoryAxis.categoryNames = catNames + drawing.add(bc, 'barchart') + + return drawing + + +def sampleH2a(): + "Sample of multi-series bar chart." + + data = [(2.4, -5.7, 2, 5, 9.2), + (0.6, -4.9, -3, 4, 6.8) + ] + + labels = ("Q3 2000", "Year to Date", "12 months", + "Annualised\n3 years", "Since 07.10.99") + + drawing = Drawing(400, 200) + + bc = HorizontalBarChart() + bc.x = 80 + bc.y = 50 + bc.height = 120 + bc.width = 300 + bc.data = data + + bc.barSpacing = 0 + bc.groupSpacing = 10 + bc.barWidth = 10 + + bc.valueAxis.valueMin = -15 + bc.valueAxis.valueMax = +15 + bc.valueAxis.valueStep = 5 + bc.valueAxis.labels.fontName = 'Helvetica' + bc.valueAxis.labels.fontSize = 8 + bc.valueAxis.labels.boxAnchor = 'n' # irrelevant (becomes 'c') + bc.valueAxis.labels.textAnchor = 'middle' + bc.valueAxis.configure(bc.data) + + bc.categoryAxis.categoryNames = labels + bc.categoryAxis.labels.fontName = 'Helvetica' + bc.categoryAxis.labels.fontSize = 8 + bc.categoryAxis.labels.dx = -150 + + drawing.add(bc) + + return drawing + + +def sampleH2b(): + "Sample of multi-series bar chart." + + data = [(2.4, -5.7, 2, 5, 9.2), + (0.6, -4.9, -3, 4, 6.8) + ] + + labels = ("Q3 2000", "Year to Date", "12 months", + "Annualised\n3 years", "Since 07.10.99") + + drawing = Drawing(400, 200) + + bc = HorizontalBarChart() + bc.x = 80 + bc.y = 50 + bc.height = 120 + bc.width = 300 + bc.data = data + + bc.barSpacing = 5 + bc.groupSpacing = 10 + bc.barWidth = 10 + + bc.valueAxis.valueMin = -15 + bc.valueAxis.valueMax = +15 + bc.valueAxis.valueStep = 5 + bc.valueAxis.labels.fontName = 'Helvetica' + bc.valueAxis.labels.fontSize = 8 + bc.valueAxis.labels.boxAnchor = 'n' # irrelevant (becomes 'c') + bc.valueAxis.labels.textAnchor = 'middle' + + bc.categoryAxis.categoryNames = labels + bc.categoryAxis.labels.fontName = 'Helvetica' + bc.categoryAxis.labels.fontSize = 8 + bc.categoryAxis.labels.dx = -150 + + drawing.add(bc) + + return drawing + + +def sampleH2c(): + "Sample of multi-series bar chart." + + data = [(2.4, -5.7, 2, 5, 9.99), + (0.6, -4.9, -3, 4, 9.99) + ] + + labels = ("Q3 2000", "Year to Date", "12 months", + "Annualised\n3 years", "Since 07.10.99") + + drawing = Drawing(400, 200) + + bc = HorizontalBarChart() + bc.x = 80 + bc.y = 50 + bc.height = 120 + bc.width = 300 + bc.data = data + + bc.barSpacing = 2 + bc.groupSpacing = 10 + bc.barWidth = 10 + + bc.valueAxis.valueMin = -15 + bc.valueAxis.valueMax = +15 + bc.valueAxis.valueStep = 5 + bc.valueAxis.labels.fontName = 'Helvetica' + bc.valueAxis.labels.fontSize = 8 + bc.valueAxis.labels.boxAnchor = 'n' + bc.valueAxis.labels.textAnchor = 'middle' + + bc.categoryAxis.categoryNames = labels + bc.categoryAxis.labels.fontName = 'Helvetica' + bc.categoryAxis.labels.fontSize = 8 + bc.categoryAxis.labels.dx = -150 + + bc.barLabels.nudge = 10 + + bc.barLabelFormat = '%0.2f' + bc.barLabels.dx = 0 + bc.barLabels.dy = 0 + bc.barLabels.boxAnchor = 'n' # irrelevant (becomes 'c') + bc.barLabels.fontName = 'Helvetica' + bc.barLabels.fontSize = 6 + + drawing.add(bc) + + return drawing + + +def sampleH3(): + "A really horizontal bar chart (compared to the equivalent faked one)." + + names = ("UK Equities", "US Equities", "European Equities", "Japanese Equities", + "Pacific (ex Japan) Equities", "Emerging Markets Equities", + "UK Bonds", "Overseas Bonds", "UK Index-Linked", "Cash") + + series1 = (-1.5, 0.3, 0.5, 1.0, 0.8, 0.7, 0.4, 0.1, 1.0, 0.3) + series2 = (0.0, 0.33, 0.55, 1.1, 0.88, 0.77, 0.44, 0.11, 1.10, 0.33) + + assert len(names) == len(series1), "bad data" + assert len(names) == len(series2), "bad data" + + drawing = Drawing(400, 200) + + bc = HorizontalBarChart() + bc.x = 100 + bc.y = 20 + bc.height = 150 + bc.width = 250 + bc.data = (series1,) + bc.bars.fillColor = colors.green + + bc.barLabelFormat = '%0.2f' + bc.barLabels.dx = 0 + bc.barLabels.dy = 0 + bc.barLabels.boxAnchor = 'w' # irrelevant (becomes 'c') + bc.barLabels.fontName = 'Helvetica' + bc.barLabels.fontSize = 6 + bc.barLabels.nudge = 10 + + bc.valueAxis.visible = 0 + bc.valueAxis.valueMin = -2 + bc.valueAxis.valueMax = +2 + bc.valueAxis.valueStep = 1 + + bc.categoryAxis.tickLeft = 0 + bc.categoryAxis.tickRight = 0 + bc.categoryAxis.categoryNames = names + bc.categoryAxis.labels.boxAnchor = 'w' + bc.categoryAxis.labels.dx = -170 + bc.categoryAxis.labels.fontName = 'Helvetica' + bc.categoryAxis.labels.fontSize = 6 + + g = Group(bc) + drawing.add(g) + + return drawing + + +def sampleH4a(): + "A bar chart showing value axis region starting at *exactly* zero." + + drawing = Drawing(400, 200) + + data = [(13, 20)] + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleH4b(): + "A bar chart showing value axis region starting *below* zero." + + drawing = Drawing(400, 200) + + data = [(13, 20)] + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = -10 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleH4c(): + "A bar chart showing value axis region starting *above* zero." + + drawing = Drawing(400, 200) + + data = [(13, 20)] + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 10 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleH4d(): + "A bar chart showing value axis region entirely *below* zero." + + drawing = Drawing(400, 200) + + data = [(-13, -20)] + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = -30 + bc.valueAxis.valueMax = -10 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +dataSample5 = [(10, 60), (20, 50), (30, 40), (40, 30)] + +def sampleH5a(): + "A simple bar chart with no expressed spacing attributes." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleH5b(): + "A simple bar chart with proportional spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 0 + bc.barWidth = 40 + bc.groupSpacing = 20 + bc.barSpacing = 10 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleH5c1(): + "A simple bar chart with absolute spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 1 + bc.barWidth = 10 + bc.groupSpacing = 0 + bc.barSpacing = 0 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleH5c2(): + "Simple bar chart with absolute spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 1 + bc.barWidth = 10 + bc.groupSpacing = 20 + bc.barSpacing = 0 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleH5c3(): + "Simple bar chart with absolute spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 20 + bc.height = 155 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 1 + bc.barWidth = 10 + bc.groupSpacing = 0 + bc.barSpacing = 2 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleH5c4(): + "Simple bar chart with absolute spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 1 + bc.barWidth = 10 + bc.groupSpacing = 20 + bc.barSpacing = 10 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + +def sampleSymbol1(): + "Simple bar chart using symbol attribute." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.barWidth = 10 + bc.groupSpacing = 15 + bc.barSpacing = 3 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + from reportlab.graphics.widgets.grids import ShadedRect + sym1 = ShadedRect() + sym1.fillColorStart = colors.black + sym1.fillColorEnd = colors.blue + sym1.orientation = 'horizontal' + sym1.strokeWidth = 0 + + sym2 = ShadedRect() + sym2.fillColorStart = colors.black + sym2.fillColorEnd = colors.pink + sym2.orientation = 'horizontal' + sym2.strokeWidth = 0 + + sym3 = ShadedRect() + sym3.fillColorStart = colors.blue + sym3.fillColorEnd = colors.white + sym3.orientation = 'vertical' + sym3.cylinderMode = 1 + sym3.strokeWidth = 0 + + bc.bars.symbol = sym1 + bc.bars[2].symbol = sym2 + bc.bars[3].symbol = sym3 + + drawing.add(bc) + + return drawing + +def sampleStacked1(): + "Simple bar chart using symbol attribute." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = VerticalBarChart() + bc.categoryAxis.style = 'stacked' + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.barWidth = 10 + bc.groupSpacing = 15 + bc.valueAxis.valueMin = 0 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + from reportlab.graphics.widgets.grids import ShadedRect + bc.bars.symbol = ShadedRect() + bc.bars.symbol.fillColorStart = colors.red + bc.bars.symbol.fillColorEnd = colors.white + bc.bars.symbol.orientation = 'vertical' + bc.bars.symbol.cylinderMode = 1 + bc.bars.symbol.strokeWidth = 0 + + bc.bars[1].symbol = ShadedRect() + bc.bars[1].symbol.fillColorStart = colors.magenta + bc.bars[1].symbol.fillColorEnd = colors.white + bc.bars[1].symbol.orientation = 'vertical' + bc.bars[1].symbol.cylinderMode = 1 + bc.bars[1].symbol.strokeWidth = 0 + + bc.bars[2].symbol = ShadedRect() + bc.bars[2].symbol.fillColorStart = colors.green + bc.bars[2].symbol.fillColorEnd = colors.white + bc.bars[2].symbol.orientation = 'vertical' + bc.bars[2].symbol.cylinderMode = 1 + bc.bars[2].symbol.strokeWidth = 0 + + bc.bars[3].symbol = ShadedRect() + bc.bars[3].symbol.fillColorStart = colors.blue + bc.bars[3].symbol.fillColorEnd = colors.white + bc.bars[3].symbol.orientation = 'vertical' + bc.bars[3].symbol.cylinderMode = 1 + bc.bars[3].symbol.strokeWidth = 0 + + drawing.add(bc) + + return drawing + +#class version of function sampleH5c4 above +class SampleH5c4(Drawing): + "Simple bar chart with absolute spacing." + + def __init__(self,width=400,height=200,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = dataSample5 + bc.strokeColor = colors.black + + bc.useAbsolute = 1 + bc.barWidth = 10 + bc.groupSpacing = 20 + bc.barSpacing = 10 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + self.add(bc,name='HBC') diff --git a/bin/reportlab/graphics/charts/dotbox.py b/bin/reportlab/graphics/charts/dotbox.py new file mode 100644 index 00000000000..6962d278822 --- /dev/null +++ b/bin/reportlab/graphics/charts/dotbox.py @@ -0,0 +1,165 @@ +from reportlab.lib.colors import blue, _PCMYK_black +from reportlab.graphics.charts.textlabels import Label +from reportlab.graphics.shapes import Circle, Drawing, Group, Line, Rect, String +from reportlab.graphics.widgetbase import Widget +from reportlab.lib.attrmap import * +from reportlab.lib.validators import * +from reportlab.lib.units import cm +from reportlab.pdfbase.pdfmetrics import getFont +from reportlab.graphics.charts.lineplots import _maxWidth + +class DotBox(Widget): + """Returns a dotbox widget.""" + + #Doesn't use TypedPropertyCollection for labels - this can be a later improvement + _attrMap = AttrMap( + xlabels = AttrMapValue(isNoneOrListOfNoneOrStrings, + desc="List of text labels for boxes on left hand side"), + ylabels = AttrMapValue(isNoneOrListOfNoneOrStrings, + desc="Text label for second box on left hand side"), + labelFontName = AttrMapValue(isString, + desc="Name of font used for the labels"), + labelFontSize = AttrMapValue(isNumber, + desc="Size of font used for the labels"), + labelOffset = AttrMapValue(isNumber, + desc="Space between label text and grid edge"), + strokeWidth = AttrMapValue(isNumber, + desc='Width of the grid and dot outline'), + gridDivWidth = AttrMapValue(isNumber, + desc="Width of each 'box'"), + gridColor = AttrMapValue(isColor, + desc='Colour for the box and gridding'), + dotDiameter = AttrMapValue(isNumber, + desc="Diameter of the circle used for the 'dot'"), + dotColor = AttrMapValue(isColor, + desc='Colour of the circle on the box'), + dotXPosition = AttrMapValue(isNumber, + desc='X Position of the circle'), + dotYPosition = AttrMapValue(isNumber, + desc='X Position of the circle'), + x = AttrMapValue(isNumber, + desc='X Position of dotbox'), + y = AttrMapValue(isNumber, + desc='Y Position of dotbox'), + ) + + def __init__(self): + self.xlabels=["Value", "Blend", "Growth"] + self.ylabels=["Small", "Medium", "Large"] + self.labelFontName = "Helvetica" + self.labelFontSize = 6 + self.labelOffset = 5 + self.strokeWidth = 0.5 + self.gridDivWidth=0.5*cm + self.gridColor=colors.Color(25/255.0,77/255.0,135/255.0) + self.dotDiameter=0.4*cm + self.dotColor=colors.Color(232/255.0,224/255.0,119/255.0) + self.dotXPosition = 1 + self.dotYPosition = 1 + self.x = 30 + self.y = 5 + + + def _getDrawingDimensions(self): + leftPadding=rightPadding=topPadding=bottomPadding=5 + #find width of grid + tx=len(self.xlabels)*self.gridDivWidth + #add padding (and offset) + tx=tx+leftPadding+rightPadding+self.labelOffset + #add in maximum width of text + tx=tx+_maxWidth(self.xlabels, self.labelFontName, self.labelFontSize) + #find height of grid + ty=len(self.ylabels)*self.gridDivWidth + #add padding (and offset) + ty=ty+topPadding+bottomPadding+self.labelOffset + #add in maximum width of text + ty=ty+_maxWidth(self.ylabels, self.labelFontName, self.labelFontSize) + #print (tx, ty) + return (tx,ty) + + def demo(self,drawing=None): + if not drawing: + tx,ty=self._getDrawingDimensions() + drawing = Drawing(tx,ty) + drawing.add(self.draw()) + return drawing + + def draw(self): + g = Group() + + #box + g.add(Rect(self.x,self.y,len(self.xlabels)*self.gridDivWidth,len(self.ylabels)*self.gridDivWidth, + strokeColor=self.gridColor, + strokeWidth=self.strokeWidth, + fillColor=None)) + + #internal gridding + for f in range (1,len(self.ylabels)): + #horizontal + g.add(Line(strokeColor=self.gridColor, + strokeWidth=self.strokeWidth, + x1 = self.x, + y1 = self.y+f*self.gridDivWidth, + x2 = self.x+len(self.xlabels)*self.gridDivWidth, + y2 = self.y+f*self.gridDivWidth)) + for f in range (1,len(self.xlabels)): + #vertical + g.add(Line(strokeColor=self.gridColor, + strokeWidth=self.strokeWidth, + x1 = self.x+f*self.gridDivWidth, + y1 = self.y, + x2 = self.x+f*self.gridDivWidth, + y2 = self.y+len(self.ylabels)*self.gridDivWidth)) + + # draw the 'dot' + g.add(Circle(strokeColor=self.gridColor, + strokeWidth=self.strokeWidth, + fillColor=self.dotColor, + cx = self.x+(self.dotXPosition*self.gridDivWidth), + cy = self.y+(self.dotYPosition*self.gridDivWidth), + r = self.dotDiameter/2.0)) + + #used for centering y-labels (below) + ascent=getFont(self.labelFontName).face.ascent + if ascent==0: + ascent=0.718 # default (from helvetica) + ascent=ascent*self.labelFontSize # normalize + + #do y-labels + if self.ylabels != None: + for f in range (len(self.ylabels)-1,-1,-1): + if self.ylabels[f]!= None: + g.add(String(strokeColor=self.gridColor, + text = self.ylabels[f], + fontName = self.labelFontName, + fontSize = self.labelFontSize, + fillColor=_PCMYK_black, + x = self.x-self.labelOffset, + y = self.y+(f*self.gridDivWidth+(self.gridDivWidth-ascent)/2.0), + textAnchor = 'end')) + + #do x-labels + if self.xlabels != None: + for f in range (0,len(self.xlabels)): + if self.xlabels[f]!= None: + l=Label() + l.x=self.x+(f*self.gridDivWidth)+(self.gridDivWidth+ascent)/2.0 + l.y=self.y+(len(self.ylabels)*self.gridDivWidth)+self.labelOffset + l.angle=90 + l.textAnchor='start' + l.fontName = self.labelFontName + l.fontSize = self.labelFontSize + l.fillColor = _PCMYK_black + l.setText(self.xlabels[f]) + l.boxAnchor = 'sw' + l.draw() + g.add(l) + + return g + + + + +if __name__ == "__main__": + d = DotBox() + d.demo().save(fnRoot="dotbox") \ No newline at end of file diff --git a/bin/reportlab/graphics/charts/doughnut.py b/bin/reportlab/graphics/charts/doughnut.py new file mode 100644 index 00000000000..f365f9a3fb8 --- /dev/null +++ b/bin/reportlab/graphics/charts/doughnut.py @@ -0,0 +1,349 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/doughnut.py +# doughnut chart + +"""Doughnut chart + +Produces a circular chart like the doughnut charts produced by Excel. +Can handle multiple series (which produce concentric 'rings' in the chart). + +""" +__version__=''' $Id: doughnut.py 2499 2004-12-29 17:12:34Z rgbecker $ ''' + +import copy +from math import sin, cos, pi +from types import ListType, TupleType +from reportlab.lib import colors +from reportlab.lib.validators import isColor, isNumber, isListOfNumbersOrNone,\ + isListOfNumbers, isColorOrNone, isString,\ + isListOfStringsOrNone, OneOf, SequenceOf,\ + isBoolean, isListOfColors,\ + isNoneOrListOfNoneOrStrings,\ + isNoneOrListOfNoneOrNumbers,\ + isNumberOrNone +from reportlab.lib.attrmap import * +from reportlab.pdfgen.canvas import Canvas +from reportlab.graphics.shapes import Group, Drawing, Line, Rect, Polygon, Ellipse, \ + Wedge, String, SolidShape, UserNode, STATE_DEFAULTS +from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder +from reportlab.graphics.charts.piecharts import AbstractPieChart, WedgeProperties, _addWedgeLabel +from reportlab.graphics.charts.textlabels import Label +from reportlab.graphics.widgets.markers import Marker + +class SectorProperties(WedgeProperties): + """This holds descriptive information about the sectors in a doughnut chart. + + It is not to be confused with the 'sector itself'; this just holds + a recipe for how to format one, and does not allow you to hack the + angles. It can format a genuine Sector object for you with its + format method. + """ + _attrMap = AttrMap(BASE=WedgeProperties, + ) + +class Doughnut(AbstractPieChart): + _attrMap = AttrMap( + x = AttrMapValue(isNumber, desc='X position of the chart within its container.'), + y = AttrMapValue(isNumber, desc='Y position of the chart within its container.'), + width = AttrMapValue(isNumber, desc='width of doughnut bounding box. Need not be same as width.'), + height = AttrMapValue(isNumber, desc='height of doughnut bounding box. Need not be same as height.'), + data = AttrMapValue(None, desc='list of numbers defining sector sizes; need not sum to 1'), + labels = AttrMapValue(isListOfStringsOrNone, desc="optional list of labels to use for each data point"), + startAngle = AttrMapValue(isNumber, desc="angle of first slice; like the compass, 0 is due North"), + direction = AttrMapValue(OneOf('clockwise', 'anticlockwise'), desc="'clockwise' or 'anticlockwise'"), + slices = AttrMapValue(None, desc="collection of sector descriptor objects"), + simpleLabels = AttrMapValue(isBoolean, desc="If true(default) use String not super duper WedgeLabel"), + ) + + def __init__(self): + self.x = 0 + self.y = 0 + self.width = 100 + self.height = 100 + self.data = [1,1] + self.labels = None # or list of strings + self.startAngle = 90 + self.direction = "clockwise" + self.simpleLabels = 1 + + self.slices = TypedPropertyCollection(SectorProperties) + self.slices[0].fillColor = colors.darkcyan + self.slices[1].fillColor = colors.blueviolet + self.slices[2].fillColor = colors.blue + self.slices[3].fillColor = colors.cyan + + def demo(self): + d = Drawing(200, 100) + + dn = Doughnut() + dn.x = 50 + dn.y = 10 + dn.width = 100 + dn.height = 80 + dn.data = [10,20,30,40,50,60] + dn.labels = ['a','b','c','d','e','f'] + + dn.slices.strokeWidth=0.5 + dn.slices[3].popout = 10 + dn.slices[3].strokeWidth = 2 + dn.slices[3].strokeDashArray = [2,2] + dn.slices[3].labelRadius = 1.75 + dn.slices[3].fontColor = colors.red + dn.slices[0].fillColor = colors.darkcyan + dn.slices[1].fillColor = colors.blueviolet + dn.slices[2].fillColor = colors.blue + dn.slices[3].fillColor = colors.cyan + dn.slices[4].fillColor = colors.aquamarine + dn.slices[5].fillColor = colors.cadetblue + dn.slices[6].fillColor = colors.lightcoral + + d.add(dn) + return d + + def normalizeData(self, data=None): + from operator import add + sum = float(reduce(add,data,0)) + return abs(sum)>=1e-8 and map(lambda x,f=360./sum: f*x, data) or len(data)*[0] + + def makeSectors(self): + # normalize slice data + if type(self.data) in (ListType, TupleType) and type(self.data[0]) in (ListType, TupleType): + #it's a nested list, more than one sequence + normData = [] + n = [] + for l in self.data: + t = self.normalizeData(l) + normData.append(t) + n.append(len(t)) + self._seriesCount = max(n) + else: + normData = self.normalizeData(self.data) + n = len(normData) + self._seriesCount = n + + #labels + if self.labels is None: + labels = [] + if type(n) not in (ListType,TupleType): + labels = [''] * n + else: + for m in n: + labels = list(labels) + [''] * m + else: + labels = self.labels + #there's no point in raising errors for less than enough labels if + #we silently create all for the extreme case of no labels. + if type(n) not in (ListType,TupleType): + i = n-len(labels) + if i>0: + labels = list(labels) + [''] * i + else: + tlab = 0 + for m in n: + tlab = tlab+m + i = tlab-len(labels) + if i>0: + labels = list(labels) + [''] * i + + xradius = self.width/2.0 + yradius = self.height/2.0 + centerx = self.x + xradius + centery = self.y + yradius + + if self.direction == "anticlockwise": + whichWay = 1 + else: + whichWay = -1 + + g = Group() + sn = 0 + + startAngle = self.startAngle #% 360 + styleCount = len(self.slices) + if type(self.data[0]) in (ListType, TupleType): + #multi-series doughnut + iradius = (self.height/5.0)/len(self.data) + for series in normData: + i = 0 + for angle in series: + endAngle = (startAngle + (angle * whichWay)) #% 360 + if abs(startAngle-endAngle)>=1e-5: + if startAngle < endAngle: + a1 = startAngle + a2 = endAngle + else: + a1 = endAngle + a2 = startAngle + + #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 type(n) in (ListType,TupleType): + theSector = Wedge(cx, cy, xradius+(sn*iradius)-iradius, a1, a2, yradius=yradius+(sn*iradius)-iradius, radius1=yradius+(sn*iradius)-(2*iradius)) + else: + theSector = Wedge(cx, cy, xradius, a1, a2, yradius=yradius, radius1=iradius) + + theSector.fillColor = sectorStyle.fillColor + theSector.strokeColor = sectorStyle.strokeColor + theSector.strokeWidth = sectorStyle.strokeWidth + theSector.strokeDashArray = sectorStyle.strokeDashArray + + g.add(theSector) + startAngle = endAngle + + text = self.getSeriesName(i,'') + if text: + 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) + _addWedgeLabel(self,text,g.add,averageAngle,labelX,labelY,sectorStyle) + i = i + 1 + sn = sn + 1 + + else: + i = 0 + #single series doughnut + iradius = self.height/5.0 + for angle in normData: + endAngle = (startAngle + (angle * whichWay)) #% 360 + if abs(startAngle-endAngle)>=1e-5: + if startAngle < endAngle: + a1 = startAngle + a2 = endAngle + else: + a1 = endAngle + a2 = startAngle + + #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=iradius) + elif n==1: + theSector = Wedge(cx, cy, xradius, a1, a2, yradius=yradius, iradius=iradius) + + theSector.fillColor = sectorStyle.fillColor + theSector.strokeColor = sectorStyle.strokeColor + theSector.strokeWidth = sectorStyle.strokeWidth + theSector.strokeDashArray = sectorStyle.strokeDashArray + + g.add(theSector) + + # now draw a label + if labels[i] != "": + averageAngle = (a1+a2)/2.0 + aveAngleRadians = averageAngle*pi/180.0 + labelRadius = sectorStyle.labelRadius + labelX = centerx + (0.5 * self.width * cos(aveAngleRadians) * labelRadius) + labelY = centery + (0.5 * self.height * sin(aveAngleRadians) * labelRadius) + + theLabel = String(labelX, labelY, labels[i]) + theLabel.textAnchor = "middle" + theLabel.fontSize = sectorStyle.fontSize + theLabel.fontName = sectorStyle.fontName + theLabel.fillColor = sectorStyle.fontColor + + g.add(theLabel) + + startAngle = endAngle + i = i + 1 + + 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 + +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') diff --git a/bin/reportlab/graphics/charts/legends.py b/bin/reportlab/graphics/charts/legends.py new file mode 100644 index 00000000000..fae4d2b4fec --- /dev/null +++ b/bin/reportlab/graphics/charts/legends.py @@ -0,0 +1,611 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/legends.py +"""This will be a collection of legends to be used with charts. +""" +__version__=''' $Id: legends.py 2604 2005-06-08 10:12:46Z rgbecker $ ''' + +import copy, operator + +from reportlab.lib import colors +from reportlab.lib.validators import isNumber, OneOf, isString, isColorOrNone,\ + isNumberOrNone, isListOfNumbersOrNone, isStringOrNone, isBoolean,\ + NoneOr, AutoOr, isAuto, Auto, isBoxAnchor, SequenceOf +from reportlab.lib.attrmap import * +from reportlab.pdfbase.pdfmetrics import stringWidth, getFont +from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder +from reportlab.graphics.shapes import Drawing, Group, String, Rect, Line, STATE_DEFAULTS +from reportlab.graphics.charts.areas import PlotArea +from reportlab.graphics.widgets.markers import uSymbol2Symbol, isSymbol +from reportlab.lib.utils import isSeqType + + +def _getStr(s): + if isSeqType(s): + return map(str,s) + else: + return str(s) + +def _getLines(s): + if isSeqType(s): + return tuple([(x or '').split('\n') for x in s]) + else: + return (s or '').split('\n') + +def _getLineCount(s): + T = _getLines(s) + if isSeqType(s): + return max([len(x) for x in T]) + else: + return len(T) + +def _getWidth(s,fontName, fontSize, sepSpace=0): + if isSeqType(s): + sum = 0 + for t in s: + m = [stringWidth(x, fontName, fontSize) for x in t.split('\n')] + sum += m and max(m) or 0 + sum += (len(s)-1)*sepSpace + return sum + m = [stringWidth(x, fontName, fontSize) for x in s.split('\n')] + return m and max(m) or 0 + +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"), + autoYPadding = AttrMapValue(isNumber, desc="y Padding between rows if deltay=None"), + yGap = AttrMapValue(isNumber, desc="Additional gap between rows"), + dx = AttrMapValue(isNumber, desc="Width of swatch rectangle"), + dy = AttrMapValue(isNumber, desc="Height of swatch rectangle"), + columnMaximum = AttrMapValue(isNumber, desc="Max. number of items per column"), + alignment = AttrMapValue(OneOf("left", "right"), desc="Alignment of text with respect to swatches"), + colorNamePairs = AttrMapValue(None, desc="List of color/name tuples (color can also be widget)"), + fontName = AttrMapValue(isString, desc="Font name of the strings"), + fontSize = AttrMapValue(isNumber, desc="Font size of the strings"), + fillColor = AttrMapValue(isColorOrNone, desc=""), + 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') ..."), + callout = AttrMapValue(None, desc="a user callout(self,g,x,y,(color,text))"), + boxAnchor = AttrMapValue(isBoxAnchor,'Anchor point for the legend area'), + variColumn = AttrMapValue(isBoolean,'If true column widths may vary (default is false)'), + 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'), + dividerWidth = AttrMapValue(isNumber, desc="dividerLines width"), + dividerColor = AttrMapValue(isColorOrNone, desc="dividerLines color"), + dividerDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array for dividerLines.'), + dividerOffsX = AttrMapValue(SequenceOf(isNumber,emptyOK=0,lo=2,hi=2), desc='divider lines X offsets'), + dividerOffsY = AttrMapValue(isNumber, desc="dividerLines Y offset"), + sepSpace = AttrMapValue(isNumber, desc="separator spacing"), + colEndCallout = AttrMapValue(None, desc="a user callout(self,g, x, xt, y,width, lWidth)"), + ) + + 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 + + # Distance between swatch rectangle and text. + self.dxTextSpace = 10 + + # Max. number of items per column. + self.columnMaximum = 3 + + # Color/name pairs. + self.colorNamePairs = [ (colors.red, "red"), + (colors.blue, "blue"), + (colors.green, "green"), + (colors.pink, "pink"), + (colors.yellow, "yellow") ] + + # Font name and size of the labels. + self.fontName = STATE_DEFAULTS['fontName'] + self.fontSize = STATE_DEFAULTS['fontSize'] + self.fillColor = STATE_DEFAULTS['fillColor'] + self.strokeColor = STATE_DEFAULTS['strokeColor'] + self.strokeWidth = STATE_DEFAULTS['strokeWidth'] + self.swatchMarker = None + self.boxAnchor = 'nw' + self.yGap = 0 + self.variColumn = 0 + self.dividerLines = 0 + self.dividerWidth = 0.5 + self.dividerDashArray = None + self.dividerColor = colors.black + self.dividerOffsX = (0,0) + self.dividerOffsY = 0 + self.sepSpace = 0 + self.colEndCallout = 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 = colorNamePairs.chart + texts = [str(chart.getSeriesName(i,'series %d' % i)) for i in xrange(chart._seriesCount)] + return texts + + def _calculateMaxWidth(self, colorNamePairs): + "Calculate the maximum width of some given strings." + M = [] + a = M.append + for t in self._getTexts(colorNamePairs): + M.append(_getWidth(t, self.fontName, self.fontSize,self.sepSpace)) + if not M: return 0 + if self.variColumn: + columnMaximum = self.columnMaximum + return [max(M[r:r+columnMaximum]) for r in range(0,len(M),self.columnMaximum)] + else: + return max(M) + + def _calcHeight(self): + dy = self.dy + yGap = self.yGap + thisy = upperlefty = self.y - dy + fontSize = self.fontSize + ascent=getFont(self.fontName).face.ascent/1000. + if ascent==0: ascent=0.718 # default (from helvetica) + ascent *= fontSize + leading = fontSize*1.2 + deltay = self.deltay + if not deltay: deltay = max(dy,leading)+self.autoYPadding + columnCount = 0 + count = 0 + lowy = upperlefty + lim = self.columnMaximum - 1 + for name in self._getTexts(self.colorNamePairs): + y0 = thisy+(dy-ascent)*0.5 + y = y0 - _getLineCount(name)*leading + leadingMove = 2*y0-y-thisy + newy = thisy-max(deltay,leadingMove)-yGap + lowy = min(y,newy,lowy) + if count==lim: + count = 0 + thisy = upperlefty + columnCount = columnCount + 1 + else: + thisy = newy + count = count+1 + return upperlefty - lowy + + def _defaultSwatch(self,x,thisy,dx,dy,fillColor,strokeWidth,strokeColor): + return Rect(x, thisy, dx, dy, + fillColor = fillColor, + strokeColor = strokeColor, + strokeWidth = strokeWidth, + ) + + def draw(self): + colorNamePairs = self.colorNamePairs + autoCP = isAuto(colorNamePairs) + if autoCP: + chart = getattr(colorNamePairs,'chart',getattr(colorNamePairs,'obj',None)) + swatchMarker = None + autoCP = Auto(obj=chart) + n = chart._seriesCount + chartTexts = self._getTexts(colorNamePairs) + else: + swatchMarker = getattr(self,'swatchMarker',None) + if isAuto(swatchMarker): + chart = getattr(swatchMarker,'chart',getattr(swatchMarker,'obj',None)) + swatchMarker = Auto(obj=chart) + n = len(colorNamePairs) + dx = self.dx + dy = self.dy + alignment = self.alignment + columnMaximum = self.columnMaximum + deltax = self.deltax + deltay = self.deltay + dxTextSpace = self.dxTextSpace + fontName = self.fontName + fontSize = self.fontSize + fillColor = self.fillColor + strokeWidth = self.strokeWidth + strokeColor = self.strokeColor + leading = fontSize*1.2 + yGap = self.yGap + if not deltay: + deltay = max(dy,leading)+self.autoYPadding + ba = self.boxAnchor + baw = ba not in ('nw','w','sw','autox') + maxWidth = self._calculateMaxWidth(colorNamePairs) + nCols = int((n+columnMaximum-1)/columnMaximum) + xW = dx+dxTextSpace+self.autoXPadding + variColumn = self.variColumn + if variColumn: + width = reduce(operator.add,maxWidth,0)+xW*nCols + else: + deltax = max(maxWidth+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 baw: + if ba in ('n','c','s'): + thisx -= width/2 + else: + thisx -= width + upperlefty = thisy + + g = Group() + def gAdd(t,g=g,fontName=fontName,fontSize=fontSize,fillColor=fillColor): + t.fontName = fontName + t.fontSize = fontSize + t.fillColor = fillColor + return g.add(t) + + 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) + dividerLines = self.dividerLines + if dividerLines: + dividerWidth = self.dividerWidth + dividerColor = self.dividerColor + dividerDashArray = self.dividerDashArray + dividerOffsX = self.dividerOffsX + dividerOffsY = self.dividerOffsY + + for i in xrange(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 = [] + j = int(i/columnMaximum) + + # 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": + if isSeqType(name): + for t in T[0]: + S.append(String(thisx,y,t,fontName=fontName,fontSize=fontSize,fillColor=fillColor, + textAnchor = "start")) + y -= leading + yd = y + y = y0 + for t in T[1]: + S.append(String(thisx+maxWidth[j],y,t,fontName=fontName,fontSize=fontSize,fillColor=fillColor, + textAnchor = "end")) + y -= leading + y = min(yd,y) + else: + for t in T: + # align text to left + S.append(String(thisx+maxWidth[j],y,t,fontName=fontName,fontSize=fontSize,fillColor=fillColor, + textAnchor = "end")) + y -= leading + x = thisx+maxWidth[j]+dxTextSpace + elif alignment == "right": + if isSeqType(name): + y0 = y + for t in T[0]: + S.append(String(thisx+dx+dxTextSpace,y,t,fontName=fontName,fontSize=fontSize,fillColor=fillColor, + textAnchor = "start")) + y -= leading + yd = y + y = y0 + for t in T[1]: + S.append(String(thisx+dx+dxTextSpace+maxWidth[j],y,t,fontName=fontName,fontSize=fontSize,fillColor=fillColor, + textAnchor = "end")) + y -= leading + y = min(yd,y) + else: + for t in T: + # align text to right + S.append(String(thisx+dx+dxTextSpace,y,t,fontName=fontName,fontSize=fontSize,fillColor=fillColor, + textAnchor = "start")) + y -= leading + x = thisx + else: + raise ValueError, "bad alignment" + leadingMove = 2*y0-y-thisy + + if dividerLines: + xd = thisx+dx+dxTextSpace+maxWidth[j]+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... + if isAuto(col): + chart = getattr(col,'chart',getattr(col,'obj',None)) + g.add(chart.makeSwatchSample(getattr(col,'index',i),x,thisy,dx,dy)) + elif isinstance(col, colors.Color): + if isSymbol(swatchMarker): + g.add(uSymbol2Symbol(swatchMarker,x+dx/2.,thisy+dy/2.,col)) + else: + g.add(self._defaultSwatch(x,thisy,dx,dy,fillColor=col,strokeWidth=strokeWidth,strokeColor=strokeColor)) + else: + try: + c = copy.deepcopy(col) + c.x = x + c.y = thisy + c.width = dx + c.height = dy + g.add(c) + except: + pass + + map(gAdd,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, maxWidth[j], maxWidth[j]+dx+dxTextSpace) + + if i%columnMaximum==lim: + if variColumn: + thisx += maxWidth[j]+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 = map(lambda i:(getattr(colors, i), i), items) + legend.colorNamePairs = items + + d.add(legend, 'legend') + + return d + +class TotalAnnotator: + def __init__(self, lText='Total', rText='0.0', fontName='Times-Roman', fontSize=10, + fillColor=colors.black, strokeWidth=0.5, strokeColor=colors.black, strokeDashArray=None, + dx=0, dy=0, dly=0, dlx=(0,0)): + self.lText = lText + self.rText = rText + self.fontName = fontName + self.fontSize = fontSize + self.fillColor = fillColor + self.dy = dy + self.dx = dx + self.dly = dly + self.dlx = dlx + self.strokeWidth = strokeWidth + self.strokeColor = strokeColor + self.strokeDashArray = strokeDashArray + + def __call__(self,legend, g, x, xt, y, width, lWidth): + from reportlab.graphics.shapes import String, Line + fontSize = self.fontSize + fontName = self.fontName + fillColor = self.fillColor + strokeColor = self.strokeColor + strokeWidth = self.strokeWidth + ascent=getFont(fontName).face.ascent/1000. + if ascent==0: ascent=0.718 # default (from helvetica) + ascent *= fontSize + leading = fontSize*1.2 + yt = y+self.dy-ascent*1.3 + if self.lText and fillColor: + g.add(String(xt,yt,self.lText, + fontName=fontName, + fontSize=fontSize, + fillColor=fillColor, + textAnchor = "start")) + if self.rText: + g.add(String(xt+width,yt,self.rText, + fontName=fontName, + fontSize=fontSize, + fillColor=fillColor, + textAnchor = "end")) + if strokeWidth and strokeColor: + yL = y+self.dly-leading + g.add(Line(x+self.dlx[0],yL,x+self.dlx[1]+lWidth,yL, + strokeColor=strokeColor, strokeWidth=strokeWidth, + strokeDashArray=self.strokeDashArray)) + +class LineSwatch(Widget): + """basically a Line with properties added so it can be used in a LineLegend""" + _attrMap = AttrMap( + x = AttrMapValue(isNumber, desc="x-coordinate for swatch line start point"), + y = AttrMapValue(isNumber, desc="y-coordinate for swatch line start point"), + width = AttrMapValue(isNumber, desc="length of swatch line"), + height = AttrMapValue(isNumber, desc="used for line strokeWidth"), + strokeColor = AttrMapValue(isColorOrNone, desc="color of swatch line"), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc="dash array for swatch line"), + ) + + def __init__(self): + from reportlab.lib.colors import red + from reportlab.graphics.shapes import Line + self.x = 0 + self.y = 0 + self.width = 20 + self.height = 1 + self.strokeColor = red + self.strokeDashArray = None + + def draw(self): + l = Line(self.x,self.y,self.x+self.width,self.y) + l.strokeColor = self.strokeColor + l.strokeDashArray = self.strokeDashArray + l.strokeWidth = self.height + return l + +class LineLegend(Legend): + """A subclass of Legend for drawing legends with lines as the + swatches rather than rectangles. Useful for lineCharts and + linePlots. Should be similar in all other ways the the standard + Legend class. + """ + + def __init__(self): + Legend.__init__(self) + + # Size of swatch rectangle. + self.dx = 10 + self.dy = 2 + + def _defaultSwatch(self,x,thisy,dx,dy,fillColor,strokeWidth,strokeColor): + l = LineSwatch() + l.x = x + l.y = thisy + l.width = dx + l.height = dy + l.strokeColor = fillColor + return l + +def sample1c(): + "Make sample legend." + + d = Drawing(200, 100) + + legend = Legend() + legend.alignment = 'right' + legend.x = 0 + legend.y = 100 + legend.dxTextSpace = 5 + items = 'red green blue yellow pink black white'.split() + items = map(lambda i:(getattr(colors, i), i), items) + legend.colorNamePairs = items + + d.add(legend, 'legend') + + return d + + +def sample2c(): + "Make sample legend." + + d = Drawing(200, 100) + + legend = Legend() + legend.alignment = 'right' + legend.x = 20 + legend.y = 90 + legend.deltax = 60 + legend.dxTextSpace = 10 + legend.columnMaximum = 4 + items = 'red green blue yellow pink black white'.split() + items = map(lambda i:(getattr(colors, i), i), items) + legend.colorNamePairs = items + + d.add(legend, 'legend') + + return d + +def sample3(): + "Make sample legend with line swatches." + + d = Drawing(200, 100) + + legend = LineLegend() + legend.alignment = 'right' + legend.x = 20 + legend.y = 90 + legend.deltax = 60 + legend.dxTextSpace = 10 + legend.columnMaximum = 4 + items = 'red green blue yellow pink black white'.split() + items = map(lambda i:(getattr(colors, i), i), items) + legend.colorNamePairs = items + d.add(legend, 'legend') + + return d + + +def sample3a(): + "Make sample legend with line swatches and dasharrays on the lines." + + d = Drawing(200, 100) + + legend = LineLegend() + legend.alignment = 'right' + legend.x = 20 + legend.y = 90 + legend.deltax = 60 + legend.dxTextSpace = 10 + legend.columnMaximum = 4 + items = 'red green blue yellow pink black white'.split() + darrays = ([2,1], [2,5], [2,2,5,5], [1,2,3,4], [4,2,3,4], [1,2,3,4,5,6], [1]) + cnp = [] + for i in range(0, len(items)): + l = LineSwatch() + l.strokeColor = getattr(colors, items[i]) + l.strokeDashArray = darrays[i] + cnp.append((l, items[i])) + legend.colorNamePairs = cnp + d.add(legend, 'legend') + + return d diff --git a/bin/reportlab/graphics/charts/linecharts.py b/bin/reportlab/graphics/charts/linecharts.py new file mode 100644 index 00000000000..99ef982a644 --- /dev/null +++ b/bin/reportlab/graphics/charts/linecharts.py @@ -0,0 +1,695 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/linecharts.py +""" +This modules defines a very preliminary Line Chart example. +""" +__version__=''' $Id: linecharts.py 2493 2004-12-22 16:14:25Z rgbecker $ ''' + +import string +from types import FunctionType, StringType + +from reportlab.lib import colors +from reportlab.lib.validators import isNumber, isColor, isColorOrNone, isListOfStrings, \ + isListOfStringsOrNone, SequenceOf, isBoolean, NoneOr, \ + isListOfNumbersOrNone, isStringOrNone +from reportlab.lib.attrmap import * +from reportlab.lib.formatters import Formatter +from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder +from reportlab.graphics.shapes import Line, Rect, Group, Drawing, Polygon, PolyLine +from reportlab.graphics.widgets.signsandsymbols import NoEntry +from reportlab.graphics.charts.axes import XCategoryAxis, YValueAxis +from reportlab.graphics.charts.textlabels import Label +from reportlab.graphics.widgets.markers import uSymbol2Symbol, isSymbol, makeMarker +from reportlab.graphics.charts.areas import PlotArea + +class LineChartProperties(PropHolder): + _attrMap = AttrMap( + strokeWidth = AttrMapValue(isNumber, desc='Width of a line.'), + strokeColor = AttrMapValue(isColorOrNone, desc='Color of a line.'), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array of a line.'), + symbol = AttrMapValue(NoneOr(isSymbol), desc='Widget placed at data points.'), + shader = AttrMapValue(None, desc='Shader Class.'), + filler = AttrMapValue(None, desc='Filler Class.'), + name = AttrMapValue(isStringOrNone, desc='Name of the line.'), + ) + +class AbstractLineChart(PlotArea): + + def makeSwatchSample(self,rowNo, x, y, width, height): + baseStyle = self.lines + styleIdx = rowNo % len(baseStyle) + style = baseStyle[styleIdx] + color = style.strokeColor + y = y+height/2. + if self.joinedLines: + dash = getattr(style, 'strokeDashArray', getattr(baseStyle,'strokeDashArray',None)) + strokeWidth= getattr(style, 'strokeWidth', getattr(style, 'strokeWidth',None)) + L = Line(x,y,x+width,y,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.,y,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 getattr(self.lines[i],'name',default) + +class LineChart(AbstractLineChart): + pass + +# This is conceptually similar to the VerticalBarChart. +# Still it is better named HorizontalLineChart... :-/ + +class HorizontalLineChart(LineChart): + """Line chart with multiple lines. + + A line chart is assumed to have one category and one value axis. + Despite its generic name this particular line chart class has + a vertical value axis and a horizontal category one. It may + evolve into individual horizontal and vertical variants (like + with the existing bar charts). + + Available attributes are: + + x: x-position of lower-left chart origin + y: y-position of lower-left chart origin + width: chart width + height: chart height + + useAbsolute: disables auto-scaling of chart elements (?) + lineLabelNudge: distance of data labels to data points + lineLabels: labels associated with data values + lineLabelFormat: format string or callback function + groupSpacing: space between categories + + joinedLines: enables drawing of lines + + strokeColor: color of chart lines (?) + fillColor: color for chart background (?) + lines: style list, used cyclically for data series + + valueAxis: value axis object + categoryAxis: category axis object + categoryNames: category names + + data: chart data, a list of data series of equal length + """ + + _attrMap = AttrMap(BASE=LineChart, + useAbsolute = AttrMapValue(isNumber, desc='Flag to use absolute spacing values.'), + lineLabelNudge = AttrMapValue(isNumber, desc='Distance between a data point and its label.'), + 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.'), + reversePlotOrder = AttrMapValue(isBoolean, desc='If true reverse plot order.'), + annotations = AttrMapValue(None, desc='list of callables, will be called with self, xscale, yscale.'), + ) + + def __init__(self): + LineChart.__init__(self) + + # Allow for a bounding rectangle. + self.strokeColor = None + self.fillColor = None + + # Named so we have less recoding for the horizontal one :-) + self.categoryAxis = XCategoryAxis() + self.valueAxis = YValueAxis() + + # This defines two series of 3 points. Just an example. + self.data = [(100,110,120,130), + (70, 80, 80, 90)] + self.categoryNames = ('North','South','East','West') + + self.lines = TypedPropertyCollection(LineChartProperties) + self.lines.strokeWidth = 1 + self.lines[0].strokeColor = colors.red + self.lines[1].strokeColor = colors.green + self.lines[2].strokeColor = colors.blue + + # control spacing. if useAbsolute = 1 then + # the next parameters are in points; otherwise + # they are 'proportions' and are normalized to + # fit the available space. + self.useAbsolute = 0 #- not done yet + self.groupSpacing = 1 #5 + + self.lineLabels = TypedPropertyCollection(Label) + self.lineLabelFormat = None + self.lineLabelArray = None + + # This says whether the origin is above or below + # the data point. +10 means put the origin ten points + # above the data point if value > 0, or ten + # points below if data value < 0. This is different + # to label dx/dy which are not dependent on the + # sign of the data. + self.lineLabelNudge = 10 + # If you have multiple series, by default they butt + # together. + + # New line chart attributes. + self.joinedLines = 1 # Connect items with straight lines. + self.inFill = 0 + self.reversePlotOrder = 0 + + + def demo(self): + """Shows basic use of a line chart.""" + + drawing = Drawing(200, 100) + + data = [ + (13, 5, 20, 22, 37, 45, 19, 4), + (14, 10, 21, 28, 38, 46, 25, 5) + ] + + lc = HorizontalLineChart() + + lc.x = 20 + lc.y = 10 + lc.height = 85 + lc.width = 170 + lc.data = data + lc.lines.symbol = makeMarker('Circle') + + drawing.add(lc) + + return drawing + + + def calcPositions(self): + """Works out where they go. + + Sets an attribute _positions which is a list of + lists of (x, y) matching the data. + """ + + self._seriesCount = len(self.data) + self._rowLength = max(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._positions = [] + for rowNo in range(len(self.data)): + lineRow = [] + for colNo in range(len(self.data[rowNo])): + datum = self.data[rowNo][colNo] + if datum is not None: + (groupX, groupWidth) = self.categoryAxis.scale(colNo) + x = groupX + (0.5 * self.groupSpacing * normFactor) + y = self.valueAxis.scale(0) + height = self.valueAxis.scale(datum) - y + lineRow.append((x, y+height)) + self._positions.append(lineRow) + + + def _innerDrawLabel(self, rowNo, colNo, x, y): + "Draw a label for a given item in the list." + + labelFmt = self.lineLabelFormat + labelValue = self.data[rowNo][colNo] + + if labelFmt is None: + labelText = None + elif type(labelFmt) is StringType: + if labelFmt == 'values': + labelText = self.lineLabelArray[rowNo][colNo] + else: + labelText = labelFmt % labelValue + elif type(labelFmt) is FunctionType: + labelText = labelFmt(labelValue) + elif isinstance(labelFmt, Formatter): + labelText = labelFmt(labelValue) + else: + msg = "Unknown formatter type %s, expected string or function" + raise Exception, msg % labelFmt + + if labelText: + label = self.lineLabels[(rowNo, colNo)] + # 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 = range(len(self._positions)) + if self.reversePlotOrder: P.reverse() + inFill = self.inFill + if inFill: + inFillY = self.categoryAxis._y + inFillX0 = self.valueAxis._x + inFillX1 = inFillX0 + self.categoryAxis._length + inFillG = getattr(self,'_inFillG',g) + + # Iterate over data rows. + for rowNo in P: + row = self._positions[rowNo] + styleCount = len(self.lines) + styleIdx = rowNo % styleCount + rowStyle = self.lines[styleIdx] + rowColor = rowStyle.strokeColor + dash = getattr(rowStyle, 'strokeDashArray', None) + + 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: + points = [] + for colNo in range(len(row)): + points += row[colNo] + if inFill: + points = points + [inFillX1,inFillY,inFillX0,inFillY] + inFillG.add(Polygon(points,fillColor=rowColor,strokeColor=rowColor,strokeWidth=0.1)) + else: + line = PolyLine(points,strokeColor=rowColor,strokeLineCap=0,strokeLineJoin=1) + if strokeWidth: + line.strokeWidth = strokeWidth + if dash: + line.strokeDashArray = dash + g.add(line) + + if hasattr(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(len(row)): + x1, y1 = row[colNo] + symbol = uSymbol2Symbol(uSymbol,x1,y1,rowStyle.strokeColor) + if symbol: g.add(symbol) + + # Draw item labels. + for colNo in range(len(row)): + x1, y1 = row[colNo] + self.drawLabel(g, rowNo, colNo, x1, y1) + + return g + + def draw(self): + "Draws itself." + + vA, cA = self.valueAxis, self.categoryAxis + vA.setPosition(self.x, self.y, self.height) + if vA: vA.joinAxis = cA + if cA: cA.joinAxis = vA + vA.configure(self.data) + + # If zero is in chart, put x axis there, otherwise + # use bottom. + xAxisCrossesAt = vA.scale(0) + if ((xAxisCrossesAt > self.y + self.height) or (xAxisCrossesAt < self.y)): + y = self.y + else: + y = xAxisCrossesAt + + cA.setPosition(self.x, y, self.width) + cA.configure(self.data) + + self.calcPositions() + + g = Group() + g.add(self.makeBackground()) + if self.inFill: + self._inFillG = Group() + g.add(self._inFillG) + + g.add(cA) + g.add(vA) + vA.gridStart = cA._x + vA.gridEnd = cA._x+cA._length + cA.gridStart = vA._y + cA.gridEnd = vA._y+vA._length + cA.makeGrid(g,parent=self) + vA.makeGrid(g,parent=self) + g.add(self.makeLines()) + for a in getattr(self,'annotations',()): g.add(a(self,cA.scale,vA.scale)) + return g + +def _cmpFakeItem(a,b): + '''t, z0, z1, x, y = a[:5]''' + return cmp((-a[1],a[3],a[0],-a[4]),(-b[1],b[3],b[0],-b[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(_cmpFakeItem) + #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 = 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 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 xrange(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 xrange(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 xrange(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() + map(lambda x,a=g.add: a(x[-1]),F.value()) + return g + +class VerticalLineChart(LineChart): + pass + + +def sample1(): + drawing = Drawing(400, 200) + + data = [ + (13, 5, 20, 22, 37, 45, 19, 4), + (5, 20, 46, 38, 23, 21, 6, 14) + ] + + lc = HorizontalLineChart() + + lc.x = 50 + lc.y = 50 + lc.height = 125 + lc.width = 300 + lc.data = data + lc.joinedLines = 1 + lc.lines.symbol = makeMarker('FilledDiamond') + lc.lineLabelFormat = '%2.0f' + + catNames = string.split('Jan Feb Mar Apr May Jun Jul Aug', ' ') + 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 = string.split('Jan Feb Mar Apr May Jun Jul Aug', ' ') + 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 = string.split('Jan Feb Mar Apr May Jun Jul Aug', ' ') + 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 = string.split('Jan Feb Mar Apr May Jun Jul Aug', ' ') + 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 diff --git a/bin/reportlab/graphics/charts/lineplots.py b/bin/reportlab/graphics/charts/lineplots.py new file mode 100644 index 00000000000..42732dddde3 --- /dev/null +++ b/bin/reportlab/graphics/charts/lineplots.py @@ -0,0 +1,1083 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/lineplots.py +"""This module defines a very preliminary Line Plot example. +""" +__version__=''' $Id: lineplots.py 2659 2005-08-18 10:28:12Z rgbecker $ ''' + +import string, time +from types import FunctionType + +from reportlab.lib import colors +from reportlab.lib.validators import * +from reportlab.lib.attrmap import * +from reportlab.graphics.shapes import Drawing, Group, Rect, Line, PolyLine, Polygon, _SetKeyWordArgs +from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder +from reportlab.graphics.charts.textlabels import Label +from reportlab.graphics.charts.axes import XValueAxis, YValueAxis, AdjYValueAxis, NormalDateXValueAxis +from reportlab.graphics.charts.utils import * +from reportlab.graphics.widgets.markers import uSymbol2Symbol, isSymbol, makeMarker +from reportlab.graphics.widgets.grids import Grid, DoubleGrid, ShadedRect, ShadedPolygon +from reportlab.pdfbase.pdfmetrics import stringWidth, getFont +from reportlab.graphics.charts.areas import PlotArea + +# This might be moved again from here... +class LinePlotProperties(PropHolder): + _attrMap = AttrMap( + strokeWidth = AttrMapValue(isNumber, desc='Width of a line.'), + strokeColor = AttrMapValue(isColorOrNone, desc='Color of a line.'), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array of a line.'), + symbol = AttrMapValue(None, desc='Widget placed at data points.'), + shader = AttrMapValue(None, desc='Shader Class.'), + filler = AttrMapValue(None, desc='Filler Class.'), + name = AttrMapValue(isStringOrNone, desc='Name of the line.'), + ) + +class Shader(_SetKeyWordArgs): + _attrMap = AttrMap(BASE=PlotArea, + vertical = AttrMapValue(isBoolean, desc='If true shade to x axis'), + colors = AttrMapValue(SequenceOf(isColorOrNone,lo=2,hi=2), desc='(AxisColor, LineColor)'), + ) + + def shade(self, lp, g, rowNo, rowColor, row): + c = [None,None] + c = getattr(self,'colors',c) or c + if not c[0]: c[0] = getattr(lp,'fillColor',colors.white) + if not c[1]: c[1] = rowColor + +class NoFiller: + def fill(self, lp, g, rowNo, rowColor, points): + pass + +class Filler: + '''mixin providing simple polygon fill''' + _attrMap = AttrMap( + fillColor = AttrMapValue(isColorOrNone, desc='filler interior color'), + strokeColor = AttrMapValue(isColorOrNone, desc='filler edge color'), + strokeWidth = AttrMapValue(isNumberOrNone, desc='filler edge width'), + ) + def __init__(self,**kw): + self.__dict__ = kw + + def fill(self, lp, g, rowNo, rowColor, points): + g.add(Polygon(points, + fillColor=getattr(self,'fillColor',rowColor), + strokeColor=getattr(self,'strokeColor',rowColor), + strokeWidth=getattr(self,'strokeWidth',0.1))) + +class ShadedPolyFiller(Filler,ShadedPolygon): + pass + +class PolyFiller(Filler,Polygon): + pass + +from linecharts import AbstractLineChart +class LinePlot(AbstractLineChart): + """Line plot with multiple lines. + + Both x- and y-axis are value axis (so there are no seperate + X and Y versions of this class). + """ + _attrMap = AttrMap(BASE=PlotArea, + reversePlotOrder = AttrMapValue(isBoolean, desc='If true reverse plot order.'), + lineLabelNudge = AttrMapValue(isNumber, desc='Distance between a data point and its label.'), + 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.'), + joinedLines = AttrMapValue(isNumber, desc='Display data points joined with lines if true.'), + strokeColor = AttrMapValue(isColorOrNone, desc='Color used for background border of plot area.'), + fillColor = AttrMapValue(isColorOrNone, desc='Color used for background interior of plot area.'), + lines = AttrMapValue(None, desc='Handle of the lines.'), + xValueAxis = AttrMapValue(None, desc='Handle of the x axis.'), + yValueAxis = AttrMapValue(None, desc='Handle of the y axis.'), + data = AttrMapValue(None, desc='Data to be plotted, list of (lists of) x/y tuples.'), + annotations = AttrMapValue(None, desc='list of callables, will be called with self, xscale, yscale.'), + ) + + def __init__(self): + PlotArea.__init__(self) + self.reversePlotOrder = 0 + + self.xValueAxis = XValueAxis() + self.yValueAxis = YValueAxis() + + # this defines two series of 3 points. Just an example. + self.data = [ + ((1,1), (2,2), (2.5,1), (3,3), (4,5)), + ((1,2), (2,3), (2.5,2), (3,4), (4,6)) + ] + + self.lines = TypedPropertyCollection(LinePlotProperties) + self.lines.strokeWidth = 1 + self.lines[0].strokeColor = colors.red + self.lines[1].strokeColor = colors.blue + + self.lineLabels = TypedPropertyCollection(Label) + self.lineLabelFormat = None + self.lineLabelArray = None + + # this says whether the origin is inside or outside + # the bar - +10 means put the origin ten points + # above the tip of the bar if value > 0, or ten + # points inside if bar 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. + + #private attributes + self._inFill = None + + def demo(self): + """Shows basic use of a line chart.""" + + drawing = Drawing(400, 200) + + data = [ + ((1,1), (2,2), (2.5,1), (3,3), (4,5)), + ((1,2), (2,3), (2.5,2), (3.5,5), (4,6)) + ] + + lp = LinePlot() + + lp.x = 50 + lp.y = 50 + lp.height = 125 + lp.width = 300 + lp.data = data + lp.joinedLines = 1 + lp.lineLabelFormat = '%2.0f' + lp.strokeColor = colors.black + + lp.lines[0].strokeColor = colors.red + lp.lines[0].symbol = makeMarker('FilledCircle') + lp.lines[1].strokeColor = colors.blue + lp.lines[1].symbol = makeMarker('FilledDiamond') + + lp.xValueAxis.valueMin = 0 + lp.xValueAxis.valueMax = 5 + lp.xValueAxis.valueStep = 1 + + lp.yValueAxis.valueMin = 0 + lp.yValueAxis.valueMax = 7 + lp.yValueAxis.valueStep = 1 + + drawing.add(lp) + + 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(map(len,self.data)) + + self._positions = [] + for rowNo in range(len(self.data)): + line = [] + for colNo in range(len(self.data[rowNo])): + datum = self.data[rowNo][colNo] # x,y value + if type(datum[0]) == type(''): + x = self.xValueAxis.scale(mktime(mkTimeTuple(datum[0]))) + else: + x = self.xValueAxis.scale(datum[0]) + y = self.yValueAxis.scale(datum[1]) + line.append((x, y)) + self._positions.append(line) + + 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][1] ### + + if labelFmt is None: + labelText = None + elif type(labelFmt) is StringType: + if labelFmt == 'values': + labelText = self.lineLabelArray[rowNo][colNo] + else: + labelText = labelFmt % labelValue + elif type(labelFmt) is FunctionType: + labelText = labelFmt(labelValue) + elif isinstance(labelFmt, Formatter): + labelText = labelFmt(labelValue) + else: + msg = "Unknown formatter type %s, expected string or function" + raise Exception, msg % labelFmt + + if labelText: + label = self.lineLabels[(rowNo, colNo)] + #hack to make sure labels are outside the bar + 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() + bubblePlot = getattr(self,'_bubblePlot',None) + if bubblePlot: + yA = self.yValueAxis + xA = self.xValueAxis + bubbleR = min(yA._bubbleRadius,xA._bubbleRadius) + bubbleMax = xA._bubbleMax + + labelFmt = self.lineLabelFormat + + P = range(len(self._positions)) + if self.reversePlotOrder: P.reverse() + inFill = getattr(self,'_inFill',None) + if inFill: + inFillY = self.xValueAxis._y + inFillX0 = self.yValueAxis._x + inFillX1 = inFillX0 + self.xValueAxis._length + inFillG = getattr(self,'_inFillG',g) + # Iterate over data rows. + styleCount = len(self.lines) + for rowNo in P: + row = self._positions[rowNo] + rowStyle = self.lines[rowNo % styleCount] + rowColor = rowStyle.strokeColor + dash = getattr(rowStyle, 'strokeDashArray', None) + + if hasattr(rowStyle, 'strokeWidth'): + width = rowStyle.strokeWidth + elif hasattr(self.lines, 'strokeWidth'): + width = self.lines.strokeWidth + else: + width = None + + # Iterate over data columns. + if self.joinedLines: + points = [] + for xy in row: + points = points + [xy[0], xy[1]] + if inFill: + fpoints = [inFillX0,inFillY] + points + [inFillX1,inFillY] + filler = getattr(rowStyle, 'filler', None) + if filler: + filler.fill(self,inFillG,rowNo,rowColor,fpoints) + else: + inFillG.add(Polygon(fpoints,fillColor=rowColor,strokeColor=rowColor,strokeWidth=width or 0.1)) + if inFill in (None,0,2): + line = PolyLine(points,strokeColor=rowColor,strokeLineCap=0,strokeLineJoin=1) + if width: + line.strokeWidth = width + if dash: + line.strokeDashArray = dash + g.add(line) + + if hasattr(rowStyle, 'symbol'): + uSymbol = rowStyle.symbol + elif hasattr(self.lines, 'symbol'): + uSymbol = self.lines.symbol + else: + uSymbol = None + + if uSymbol: + j = -1 + if bubblePlot: drow = self.data[rowNo] + for xy in row: + j += 1 + symbol = uSymbol2Symbol(uSymbol,xy[0],xy[1],rowColor) + if symbol: + if bubblePlot: + symbol.size = bubbleR*(drow[j][2]/bubbleMax)**0.5 + g.add(symbol) + + # Draw data labels. + for colNo in range(len(row)): + x1, y1 = row[colNo] + self.drawLabel(g, rowNo, colNo, x1, y1) + + shader = getattr(rowStyle, 'shader', None) + if shader: shader.shade(self,g,rowNo,rowColor,row) + + return g + + def draw(self): + yA = self.yValueAxis + xA = self.xValueAxis + if getattr(self,'_bubblePlot',None): + yA._bubblePlot = xA._bubblePlot = 1 + yA.setPosition(self.x, self.y, self.height) + if yA: yA.joinAxis = xA + if xA: xA.joinAxis = yA + yA.configure(self.data) + + # if zero is in chart, put x axis there, otherwise use bottom. + xAxisCrossesAt = yA.scale(0) + if ((xAxisCrossesAt > self.y + self.height) or (xAxisCrossesAt < self.y)): + y = self.y + else: + y = xAxisCrossesAt + + xA.setPosition(self.x, y, self.width) + xA.configure(self.data) + self.calcPositions() + g = Group() + g.add(self.makeBackground()) + if self._inFill: + xA._joinToAxis() + self._inFillG = Group() + g.add(self._inFillG) + g.add(xA) + g.add(yA) + yA.gridStart = xA._x + yA.gridEnd = xA._x+xA._length + xA.gridStart = yA._y + xA.gridEnd = yA._y+yA._length + xA.makeGrid(g,parent=self) + yA.makeGrid(g,parent=self) + g.add(self.makeLines()) + for a in getattr(self,'annotations',()): g.add(a(self,xA.scale,yA.scale)) + return g + +class LinePlot3D(LinePlot): + _attrMap = AttrMap(BASE=LinePlot, + 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): + LinePlot.calcPositions(self) + nSeries = self._seriesCount + zSpace = self.zSpace + zDepth = self.zDepth + if self.xValueAxis.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.xValueAxis.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): + bubblePlot = getattr(self,'_bubblePlot',None) + assert not bubblePlot, "_bubblePlot not supported for 3d yet" + #if bubblePlot: + # yA = self.yValueAxis + # xA = self.xValueAxis + # bubbleR = min(yA._bubbleRadius,xA._bubbleRadius) + # bubbleMax = xA._bubbleMax + + labelFmt = self.lineLabelFormat + positions = self._positions + + P = range(len(positions)) + if self.reversePlotOrder: P.reverse() + inFill = getattr(self,'_inFill',None) + assert not inFill, "inFill not supported for 3d yet" + #if inFill: + # inFillY = self.xValueAxis._y + # inFillX0 = self.yValueAxis._x + # inFillX1 = inFillX0 + self.xValueAxis._length + # inFillG = getattr(self,'_inFillG',g) + zDepth = self.zDepth + _zadjust = self._zadjust + theta_x = self.theta_x + theta_y = self.theta_y + from linecharts import _FakeGroup + F = _FakeGroup() + + from utils3d import _make_3d_line_info, find_intersections + if self.xValueAxis.style!='parallel_3d': + tileWidth = getattr(self,'_3d_tilewidth',1) + if getattr(self,'_find_intersections',None): + from copy import copy + fpositions = map(copy,positions) + I = find_intersections(fpositions,small=tileWidth) + ic = None + for i,j,x,y in I: + if ic!=i: + ic = i + jc = 0 + else: + jc+=1 + fpositions[i].insert(j+jc,(x,y)) + tileWidth = None + else: + fpositions = positions + else: + tileWidth = None + fpositions = positions + + # Iterate over data rows. + styleCount = len(self.lines) + for rowNo in P: + row = positions[rowNo] + n = len(row) + rowStyle = self.lines[rowNo % styleCount] + rowColor = rowStyle.strokeColor + dash = getattr(rowStyle, 'strokeDashArray', None) + z0 = self._calc_z0(rowNo) + z1 = z0 + zDepth + + if hasattr(rowStyle, 'strokeWidth'): + width = rowStyle.strokeWidth + elif hasattr(self.lines, 'strokeWidth'): + width = self.lines.strokeWidth + else: + width = None + + # Iterate over data columns. + if self.joinedLines: + if n: + frow = fpositions[rowNo] + x0, y0 = frow[0] + for colNo in xrange(1,len(frow)): + x1, y1 = frow[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(rowStyle, 'symbol'): + uSymbol = rowStyle.symbol + elif hasattr(self.lines, 'symbol'): + uSymbol = self.lines.symbol + else: + uSymbol = None + + if uSymbol: + for xy in row: + x1, y1 = row[colNo] + x1, y1 = _zadjust(x1,y1,z0) + symbol = uSymbol2Symbol(uSymbol,xy[0],xy[1],rowColor) + if symbol: F.add((1,z0,z0,x1,y1,symbol)) + + # Draw data labels. + for colNo in xrange(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() + map(lambda x,a=g.add: a(x[-1]),F.value()) + return g + +_monthlyIndexData = [[(19971202, 100.0), + (19971231, 100.1704367), + (19980131, 101.5639577), + (19980228, 102.1879927), + (19980331, 101.6337257), + (19980430, 102.7640446), + (19980531, 102.9198038), + (19980630, 103.25938789999999), + (19980731, 103.2516421), + (19980831, 105.4744329), + (19980930, 109.3242705), + (19981031, 111.9859291), + (19981130, 110.9184642), + (19981231, 110.9184642), + (19990131, 111.9882532), + (19990228, 109.7912614), + (19990331, 110.24189629999999), + (19990430, 110.4279321), + (19990531, 109.33955469999999), + (19990630, 108.2341748), + (19990731, 110.21294469999999), + (19990831, 110.9683062), + (19990930, 112.4425371), + (19991031, 112.7314032), + (19991130, 112.3509645), + (19991231, 112.3660659), + (20000131, 110.9255248), + (20000229, 110.5266306), + (20000331, 113.3116101), + (20000430, 111.0449133), + (20000531, 111.702717), + (20000630, 113.5832178)], + [(19971202, 100.0), + (19971231, 100.0), + (19980131, 100.8), + (19980228, 102.0), + (19980331, 101.9), + (19980430, 103.0), + (19980531, 103.0), + (19980630, 103.1), + (19980731, 103.1), + (19980831, 102.8), + (19980930, 105.6), + (19981031, 108.3), + (19981130, 108.1), + (19981231, 111.9), + (19990131, 113.1), + (19990228, 110.2), + (19990331, 111.8), + (19990430, 112.3), + (19990531, 110.1), + (19990630, 109.3), + (19990731, 111.2), + (19990831, 111.7), + (19990930, 112.6), + (19991031, 113.2), + (19991130, 113.9), + (19991231, 115.4), + (20000131, 112.7), + (20000229, 113.9), + (20000331, 115.8), + (20000430, 112.2), + (20000531, 112.6), + (20000630, 114.6)]] + +class GridLinePlot(LinePlot): + """A customized version of LinePlot. + It uses NormalDateXValueAxis() and AdjYValueAxis() for the X and Y axes. + The chart has a default grid background with thin horizontal lines + aligned with the tickmarks (and labels). You can change the back- + ground to be any Grid or ShadedRect, or scale the whole chart. + If you do provide a background, you can specify the colours of the + stripes with 'background.stripeColors'. + """ + + _attrMap = AttrMap(BASE=LinePlot, + background = AttrMapValue(None, desc='Background for chart area (now Grid or ShadedRect).'), + scaleFactor = AttrMapValue(isNumberOrNone, desc='Scalefactor to apply to whole drawing.'), + ) + + def __init__(self): + from reportlab.lib import colors + LinePlot.__init__(self) + self.xValueAxis = NormalDateXValueAxis() + self.yValueAxis = AdjYValueAxis() + self.scaleFactor = None + self.background = Grid() + self.background.orientation = 'horizontal' + self.background.useRects = 0 + self.background.useLines = 1 + self.background.strokeWidth = 0.5 + self.background.strokeColor = colors.black + self.data = _monthlyIndexData + + def demo(self,drawing=None): + from reportlab.lib import colors + if not drawing: + drawing = Drawing(400, 200) + lp = AdjLinePlot() + lp.x = 50 + lp.y = 50 + lp.height = 125 + lp.width = 300 + lp.data = _monthlyIndexData + lp.joinedLines = 1 + lp.strokeColor = colors.black + c0 = colors.PCMYKColor(100,65,0,30, spotName='PANTONE 288 CV', density=100) + lp.lines[0].strokeColor = c0 + lp.lines[0].strokeWidth = 2 + lp.lines[0].strokeDashArray = None + c1 = colors.PCMYKColor(0,79,91,0, spotName='PANTONE Wm Red CV', density=100) + lp.lines[1].strokeColor = c1 + lp.lines[1].strokeWidth = 1 + lp.lines[1].strokeDashArray = [3,1] + lp.xValueAxis.labels.fontSize = 10 + lp.xValueAxis.labels.textAnchor = 'start' + lp.xValueAxis.labels.boxAnchor = 'w' + lp.xValueAxis.labels.angle = -45 + lp.xValueAxis.labels.dx = 0 + lp.xValueAxis.labels.dy = -8 + lp.xValueAxis.xLabelFormat = '{mm}/{yy}' + lp.yValueAxis.labelTextFormat = '%5d%% ' + lp.yValueAxis.tickLeft = 5 + lp.yValueAxis.labels.fontSize = 10 + lp.background = Grid() + lp.background.stripeColors = [colors.pink, colors.lightblue] + lp.background.orientation = 'vertical' + drawing.add(lp,'plot') + return drawing + + def draw(self): + xva, yva = self.xValueAxis, self.yValueAxis + if xva: xva.joinAxis = yva + if yva: yva.joinAxis = xva + + yva.setPosition(self.x, self.y, self.height) + yva.configure(self.data) + + # if zero is in chart, put x axis there, otherwise + # use bottom. + xAxisCrossesAt = yva.scale(0) + if ((xAxisCrossesAt > self.y + self.height) or (xAxisCrossesAt < self.y)): + y = self.y + else: + y = xAxisCrossesAt + + xva.setPosition(self.x, y, self.width) + xva.configure(self.data) + + back = self.background + if isinstance(back, Grid): + if back.orientation == 'vertical' and xva._tickValues: + xpos = map(xva.scale, [xva._valueMin] + xva._tickValues) + steps = [] + for i in range(len(xpos)-1): + steps.append(xpos[i+1] - xpos[i]) + back.deltaSteps = steps + elif back.orientation == 'horizontal' and yva._tickValues: + ypos = map(yva.scale, [yva._valueMin] + yva._tickValues) + steps = [] + for i in range(len(ypos)-1): + steps.append(ypos[i+1] - ypos[i]) + back.deltaSteps = steps + elif isinstance(back, DoubleGrid): + # Ideally, these lines would not be needed... + back.grid0.x = self.x + back.grid0.y = self.y + back.grid0.width = self.width + back.grid0.height = self.height + back.grid1.x = self.x + back.grid1.y = self.y + back.grid1.width = self.width + back.grid1.height = self.height + + # some room left for optimization... + if back.grid0.orientation == 'vertical' and xva._tickValues: + xpos = map(xva.scale, [xva._valueMin] + xva._tickValues) + steps = [] + for i in range(len(xpos)-1): + steps.append(xpos[i+1] - xpos[i]) + back.grid0.deltaSteps = steps + elif back.grid0.orientation == 'horizontal' and yva._tickValues: + ypos = map(yva.scale, [yva._valueMin] + yva._tickValues) + steps = [] + for i in range(len(ypos)-1): + steps.append(ypos[i+1] - ypos[i]) + back.grid0.deltaSteps = steps + if back.grid1.orientation == 'vertical' and xva._tickValues: + xpos = map(xva.scale, [xva._valueMin] + xva._tickValues) + steps = [] + for i in range(len(xpos)-1): + steps.append(xpos[i+1] - xpos[i]) + back.grid1.deltaSteps = steps + elif back.grid1.orientation == 'horizontal' and yva._tickValues: + ypos = map(yva.scale, [yva._valueMin] + yva._tickValues) + steps = [] + for i in range(len(ypos)-1): + steps.append(ypos[i+1] - ypos[i]) + back.grid1.deltaSteps = steps + + self.calcPositions() + + width, height, scaleFactor = self.width, self.height, self.scaleFactor + if scaleFactor and scaleFactor!=1: + #g = Drawing(scaleFactor*width, scaleFactor*height) + g.transform = (scaleFactor, 0, 0, scaleFactor,0,0) + else: + g = Group() + + g.add(self.makeBackground()) + g.add(self.xValueAxis) + g.add(self.yValueAxis) + g.add(self.makeLines()) + + return g + +class AreaLinePlot(LinePlot): + '''we're given data in the form [(X1,Y11,..Y1M)....(Xn,Yn1,...YnM)]'''#' + def __init__(self): + LinePlot.__init__(self) + self._inFill = 1 + self.reversePlotOrder = 1 + self.data = [(1,20,100,30),(2,11,50,15),(3,15,70,40)] + + def draw(self): + try: + odata = self.data + n = len(odata) + m = len(odata[0]) + S = n*[0] + self.data = [] + for i in xrange(1,m): + D = [] + for j in xrange(n): + S[j] = S[j] + odata[j][i] + D.append((odata[j][0],S[j])) + self.data.append(D) + return LinePlot.draw(self) + finally: + self.data = odata + +class SplitLinePlot(AreaLinePlot): + def __init__(self): + AreaLinePlot.__init__(self) + self.xValueAxis = NormalDateXValueAxis() + self.yValueAxis = AdjYValueAxis() + self.data=[(20030601,0.95,0.05,0.0),(20030701,0.95,0.05,0.0),(20030801,0.95,0.05,0.0),(20030901,0.95,0.05,0.0),(20031001,0.95,0.05,0.0),(20031101,0.95,0.05,0.0),(20031201,0.95,0.05,0.0),(20040101,0.95,0.05,0.0),(20040201,0.95,0.05,0.0),(20040301,0.95,0.05,0.0),(20040401,0.95,0.05,0.0),(20040501,0.95,0.05,0.0),(20040601,0.95,0.05,0.0),(20040701,0.95,0.05,0.0),(20040801,0.95,0.05,0.0),(20040901,0.95,0.05,0.0),(20041001,0.95,0.05,0.0),(20041101,0.95,0.05,0.0),(20041201,0.95,0.05,0.0),(20050101,0.95,0.05,0.0),(20050201,0.95,0.05,0.0),(20050301,0.95,0.05,0.0),(20050401,0.95,0.05,0.0),(20050501,0.95,0.05,0.0),(20050601,0.95,0.05,0.0),(20050701,0.95,0.05,0.0),(20050801,0.95,0.05,0.0),(20050901,0.95,0.05,0.0),(20051001,0.95,0.05,0.0),(20051101,0.95,0.05,0.0),(20051201,0.95,0.05,0.0),(20060101,0.95,0.05,0.0),(20060201,0.95,0.05,0.0),(20060301,0.95,0.05,0.0),(20060401,0.95,0.05,0.0),(20060501,0.95,0.05,0.0),(20060601,0.95,0.05,0.0),(20060701,0.95,0.05,0.0),(20060801,0.95,0.05,0.0),(20060901,0.95,0.05,0.0),(20061001,0.95,0.05,0.0),(20061101,0.95,0.05,0.0),(20061201,0.95,0.05,0.0),(20070101,0.95,0.05,0.0),(20070201,0.95,0.05,0.0),(20070301,0.95,0.05,0.0),(20070401,0.95,0.05,0.0),(20070501,0.95,0.05,0.0),(20070601,0.95,0.05,0.0),(20070701,0.95,0.05,0.0),(20070801,0.95,0.05,0.0),(20070901,0.95,0.05,0.0),(20071001,0.95,0.05,0.0),(20071101,0.95,0.05,0.0),(20071201,0.95,0.05,0.0),(20080101,0.95,0.05,0.0),(20080201,0.95,0.05,0.0),(20080301,0.95,0.05,0.0),(20080401,0.95,0.05,0.0),(20080501,0.95,0.05,0.0),(20080601,0.95,0.05,0.0),(20080701,0.95,0.05,0.0),(20080801,0.95,0.05,0.0),(20080901,0.95,0.05,0.0),(20081001,0.95,0.05,0.0),(20081101,0.95,0.05,0.0),(20081201,0.95,0.05,0.0),(20090101,0.95,0.05,0.0),(20090201,0.91,0.09,0.0),(20090301,0.91,0.09,0.0),(20090401,0.91,0.09,0.0),(20090501,0.91,0.09,0.0),(20090601,0.91,0.09,0.0),(20090701,0.91,0.09,0.0),(20090801,0.91,0.09,0.0),(20090901,0.91,0.09,0.0),(20091001,0.91,0.09,0.0),(20091101,0.91,0.09,0.0),(20091201,0.91,0.09,0.0),(20100101,0.91,0.09,0.0),(20100201,0.81,0.19,0.0),(20100301,0.81,0.19,0.0),(20100401,0.81,0.19,0.0),(20100501,0.81,0.19,0.0),(20100601,0.81,0.19,0.0),(20100701,0.81,0.19,0.0),(20100801,0.81,0.19,0.0),(20100901,0.81,0.19,0.0),(20101001,0.81,0.19,0.0),(20101101,0.81,0.19,0.0),(20101201,0.81,0.19,0.0),(20110101,0.81,0.19,0.0),(20110201,0.72,0.28,0.0),(20110301,0.72,0.28,0.0),(20110401,0.72,0.28,0.0),(20110501,0.72,0.28,0.0),(20110601,0.72,0.28,0.0),(20110701,0.72,0.28,0.0),(20110801,0.72,0.28,0.0),(20110901,0.72,0.28,0.0),(20111001,0.72,0.28,0.0),(20111101,0.72,0.28,0.0),(20111201,0.72,0.28,0.0),(20120101,0.72,0.28,0.0),(20120201,0.53,0.47,0.0),(20120301,0.53,0.47,0.0),(20120401,0.53,0.47,0.0),(20120501,0.53,0.47,0.0),(20120601,0.53,0.47,0.0),(20120701,0.53,0.47,0.0),(20120801,0.53,0.47,0.0),(20120901,0.53,0.47,0.0),(20121001,0.53,0.47,0.0),(20121101,0.53,0.47,0.0),(20121201,0.53,0.47,0.0),(20130101,0.53,0.47,0.0),(20130201,0.44,0.56,0.0),(20130301,0.44,0.56,0.0),(20130401,0.44,0.56,0.0),(20130501,0.44,0.56,0.0),(20130601,0.44,0.56,0.0),(20130701,0.44,0.56,0.0),(20130801,0.44,0.56,0.0),(20130901,0.44,0.56,0.0),(20131001,0.44,0.56,0.0),(20131101,0.44,0.56,0.0),(20131201,0.44,0.56,0.0),(20140101,0.44,0.56,0.0),(20140201,0.36,0.5,0.14),(20140301,0.36,0.5,0.14),(20140401,0.36,0.5,0.14),(20140501,0.36,0.5,0.14),(20140601,0.36,0.5,0.14),(20140701,0.36,0.5,0.14),(20140801,0.36,0.5,0.14),(20140901,0.36,0.5,0.14),(20141001,0.36,0.5,0.14),(20141101,0.36,0.5,0.14),(20141201,0.36,0.5,0.14),(20150101,0.36,0.5,0.14),(20150201,0.3,0.41,0.29),(20150301,0.3,0.41,0.29),(20150401,0.3,0.41,0.29),(20150501,0.3,0.41,0.29),(20150601,0.3,0.41,0.29),(20150701,0.3,0.41,0.29),(20150801,0.3,0.41,0.29),(20150901,0.3,0.41,0.29),(20151001,0.3,0.41,0.29),(20151101,0.3,0.41,0.29),(20151201,0.3,0.41,0.29),(20160101,0.3,0.41,0.29),(20160201,0.26,0.36,0.38),(20160301,0.26,0.36,0.38),(20160401,0.26,0.36,0.38),(20160501,0.26,0.36,0.38),(20160601,0.26,0.36,0.38),(20160701,0.26,0.36,0.38),(20160801,0.26,0.36,0.38),(20160901,0.26,0.36,0.38),(20161001,0.26,0.36,0.38),(20161101,0.26,0.36,0.38),(20161201,0.26,0.36,0.38),(20170101,0.26,0.36,0.38),(20170201,0.2,0.3,0.5),(20170301,0.2,0.3,0.5),(20170401,0.2,0.3,0.5),(20170501,0.2,0.3,0.5),(20170601,0.2,0.3,0.5),(20170701,0.2,0.3,0.5),(20170801,0.2,0.3,0.5),(20170901,0.2,0.3,0.5),(20171001,0.2,0.3,0.5),(20171101,0.2,0.3,0.5),(20171201,0.2,0.3,0.5),(20180101,0.2,0.3,0.5),(20180201,0.13,0.37,0.5),(20180301,0.13,0.37,0.5),(20180401,0.13,0.37,0.5),(20180501,0.13,0.37,0.5),(20180601,0.13,0.37,0.5),(20180701,0.13,0.37,0.5),(20180801,0.13,0.37,0.5),(20180901,0.13,0.37,0.5),(20181001,0.13,0.37,0.5),(20181101,0.13,0.37,0.5),(20181201,0.13,0.37,0.5),(20190101,0.13,0.37,0.5),(20190201,0.1,0.4,0.5),(20190301,0.1,0.4,0.5),(20190401,0.1,0.4,0.5),(20190501,0.1,0.4,0.5),(20190601,0.1,0.4,0.5),(20190701,0.1,0.4,0.5),(20190801,0.1,0.4,0.5),(20190901,0.1,0.4,0.5),(20191001,0.1,0.4,0.5),(20191101,0.1,0.4,0.5),(20191201,0.1,0.4,0.5),(20200101,0.1,0.4,0.5)] + self.yValueAxis.requiredRange = None + self.yValueAxis.leftAxisPercent = 0 + self.yValueAxis.leftAxisOrigShiftMin = 0 + self.yValueAxis.leftAxisOrigShiftIPC = 0 + self.lines[0].strokeColor = colors.toColor(0x0033cc) + self.lines[1].strokeColor = colors.toColor(0x99c3ff) + self.lines[2].strokeColor = colors.toColor(0xCC0033) + +def _maxWidth(T, fontName, fontSize): + '''return max stringWidth for the list of strings T''' + if type(T) not in (type(()),type([])): T = (T,) + T = filter(None,T) + return T and max(map(lambda t,sW=stringWidth,fN=fontName, fS=fontSize: sW(t,fN,fS),T)) or 0 + +class ScatterPlot(LinePlot): + """A scatter plot widget""" + + _attrMap = AttrMap(BASE=LinePlot, + width = AttrMapValue(isNumber, desc="Width of the area inside the axes"), + height = AttrMapValue(isNumber, desc="Height of the area inside the axes"), + outerBorderOn = AttrMapValue(isBoolean, desc="Is there an outer border (continuation of axes)"), + outerBorderColor = AttrMapValue(isColorOrNone, desc="Color of outer border (if any)"), + background = AttrMapValue(isColorOrNone, desc="Background color (if any)"), + labelOffset = AttrMapValue(isNumber, desc="Space between label and Axis (or other labels)"), + axisTickLengths = AttrMapValue(isNumber, desc="Lenth of the ticks on both axes"), + axisStrokeWidth = AttrMapValue(isNumber, desc="Stroke width for both axes"), + xLabel = AttrMapValue(isString, desc="Label for the whole X-Axis"), + yLabel = AttrMapValue(isString, desc="Label for the whole Y-Axis"), + data = AttrMapValue(isAnything, desc='Data points - a list of x/y tuples.'), + strokeColor = AttrMapValue(isColorOrNone, desc='Color used for border of plot area.'), + fillColor = AttrMapValue(isColorOrNone, desc='Color used for background interior of plot area.'), + 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'), + ) + + def __init__(self): + LinePlot.__init__(self) + self.width = 142 + self.height = 77 + self.outerBorderOn = 1 + self.outerBorderColor = colors.black + self.background = None + + _labelOffset = 3 + _axisTickLengths = 2 + _axisStrokeWidth = 0.5 + + self.yValueAxis.valueMin = None + self.yValueAxis.valueMax = None + self.yValueAxis.valueStep = None + self.yValueAxis.labelTextFormat = '%s' + + self.xLabel="X Lable" + self.xValueAxis.labels.fontSize = 6 + + self.yLabel="Y Lable" + self.yValueAxis.labels.fontSize = 6 + + self.data =[((0.030, 62.73), + (0.074, 54.363), + (1.216, 17.964)), + + ((1.360, 11.621), + (1.387, 50.011), + (1.428, 68.953)), + + ((1.444, 86.888), + (1.754, 35.58), + (1.766, 36.05))] + + #values for lineplot + self.joinedLines = 0 + self.fillColor = self.background + + self.leftPadding=5 + self.rightPadding=10 + self.topPadding=5 + self.bottomPadding=5 + + self.x = self.leftPadding+_axisTickLengths+(_labelOffset*2) + self.x=self.x+_maxWidth(str(self.yValueAxis.valueMax), self.yValueAxis.labels.fontName, self.yValueAxis.labels.fontSize) + self.y = self.bottomPadding+_axisTickLengths+_labelOffset+self.xValueAxis.labels.fontSize + + self.xValueAxis.labels.dy = -_labelOffset + self.xValueAxis.tickDown = _axisTickLengths + self.xValueAxis.strokeWidth = _axisStrokeWidth + self.xValueAxis.rangeRound='both' + self.yValueAxis.labels.dx = -_labelOffset + self.yValueAxis.tickLeft = _axisTickLengths + self.yValueAxis.strokeWidth = _axisStrokeWidth + self.yValueAxis.rangeRound='both' + + self.lineLabelFormat="%.2f" + self.lineLabels.fontSize = 5 + self.lineLabels.boxAnchor = 'e' + self.lineLabels.dx = -2 + self.lineLabelNudge = 0 + self.lines.symbol=makeMarker('FilledCircle',size=3) + self.lines[1].symbol=makeMarker('FilledDiamond',size=3) + self.lines[2].symbol=makeMarker('FilledSquare',size=3) + self.lines[2].strokeColor = colors.green + + def _getDrawingDimensions(self): + tx = self.leftPadding+self.yValueAxis.tickLeft+(self.yValueAxis.labels.dx*2)+self.xValueAxis.labels.fontSize + tx=tx+(5*_maxWidth(str(self.yValueAxis.valueMax), self.yValueAxis.labels.fontName, self.yValueAxis.labels.fontSize)) + tx=tx+self.width+self.rightPadding + t=('%.2f%%'%self.xValueAxis.valueMax) + tx=tx+(_maxWidth(t, self.yValueAxis.labels.fontName, self.yValueAxis.labels.fontSize)) + ty = self.bottomPadding+self.xValueAxis.tickDown+(self.xValueAxis.labels.dy*2)+(self.xValueAxis.labels.fontSize*2) + ty=ty+self.yValueAxis.labels.fontSize+self.height+self.topPadding + #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): + ascent=getFont(self.xValueAxis.labels.fontName).face.ascent + if ascent==0: + ascent=0.718 # default (from helvetica) + ascent=ascent*self.xValueAxis.labels.fontSize # normalize + + #basic LinePlot - does the Axes, Ticks etc + lp = LinePlot.draw(self) + + xLabel = self.xLabel + if xLabel: #Overall label for the X-axis + xl=Label() + xl.x = (self.x+self.width)/2.0 + xl.y = 0 + xl.fontName = self.xValueAxis.labels.fontName + xl.fontSize = self.xValueAxis.labels.fontSize + xl.setText(xLabel) + lp.add(xl) + + yLabel = self.yLabel + if yLabel: #Overall label for the Y-axis + yl=Label() + yl.angle = 90 + yl.x = 0 + yl.y = (self.y+self.height/2.0) + yl.fontName = self.yValueAxis.labels.fontName + yl.fontSize = self.yValueAxis.labels.fontSize + yl.setText(yLabel) + lp.add(yl) + + # do a bounding box - in the same style as the axes + if self.outerBorderOn: + lp.add(Rect(self.x, self.y, self.width, self.height, + strokeColor = self.outerBorderColor, + strokeWidth = self.yValueAxis.strokeWidth, + fillColor = None)) + + lp.shift(self.leftPadding, self.bottomPadding) + + return lp + +def sample1a(): + "A line plot with non-equidistant points in x-axis." + + drawing = Drawing(400, 200) + + data = [ + ((1,1), (2,2), (2.5,1), (3,3), (4,5)), + ((1,2), (2,3), (2.5,2), (3.5,5), (4,6)) + ] + + lp = LinePlot() + + lp.x = 50 + lp.y = 50 + lp.height = 125 + lp.width = 300 + lp.data = data + lp.joinedLines = 1 + lp.strokeColor = colors.black + + lp.lines.symbol = makeMarker('UK_Flag') + + lp.lines[0].strokeWidth = 2 + lp.lines[1].strokeWidth = 4 + + lp.xValueAxis.valueMin = 0 + lp.xValueAxis.valueMax = 5 + lp.xValueAxis.valueStep = 1 + + lp.yValueAxis.valueMin = 0 + lp.yValueAxis.valueMax = 7 + lp.yValueAxis.valueStep = 1 + + drawing.add(lp) + + return drawing + + +def sample1b(): + "A line plot with non-equidistant points in x-axis." + + drawing = Drawing(400, 200) + + data = [ + ((1,1), (2,2), (2.5,1), (3,3), (4,5)), + ((1,2), (2,3), (2.5,2), (3.5,5), (4,6)) + ] + + lp = LinePlot() + + lp.x = 50 + lp.y = 50 + lp.height = 125 + lp.width = 300 + lp.data = data + lp.joinedLines = 1 + lp.lines.symbol = makeMarker('Circle') + lp.lineLabelFormat = '%2.0f' + lp.strokeColor = colors.black + + lp.xValueAxis.valueMin = 0 + lp.xValueAxis.valueMax = 5 + lp.xValueAxis.valueSteps = [1, 2, 2.5, 3, 4, 5] + lp.xValueAxis.labelTextFormat = '%2.1f' + + lp.yValueAxis.valueMin = 0 + lp.yValueAxis.valueMax = 7 + lp.yValueAxis.valueStep = 1 + + drawing.add(lp) + + return drawing + + +def sample1c(): + "A line plot with non-equidistant points in x-axis." + + drawing = Drawing(400, 200) + + data = [ + ((1,1), (2,2), (2.5,1), (3,3), (4,5)), + ((1,2), (2,3), (2.5,2), (3.5,5), (4,6)) + ] + + lp = LinePlot() + + lp.x = 50 + lp.y = 50 + lp.height = 125 + lp.width = 300 + lp.data = data + lp.joinedLines = 1 + lp.lines[0].symbol = makeMarker('FilledCircle') + lp.lines[1].symbol = makeMarker('Circle') + lp.lineLabelFormat = '%2.0f' + lp.strokeColor = colors.black + + lp.xValueAxis.valueMin = 0 + lp.xValueAxis.valueMax = 5 + lp.xValueAxis.valueSteps = [1, 2, 2.5, 3, 4, 5] + lp.xValueAxis.labelTextFormat = '%2.1f' + + lp.yValueAxis.valueMin = 0 + lp.yValueAxis.valueMax = 7 + lp.yValueAxis.valueSteps = [1, 2, 3, 5, 6] + + drawing.add(lp) + + return drawing + + +def preprocessData(series): + "Convert date strings into seconds and multiply values by 100." + + return map(lambda x: (str2seconds(x[0]), x[1]*100), series) + + +def sample2(): + "A line plot with non-equidistant points in x-axis." + + drawing = Drawing(400, 200) + + data = [ + (('25/11/1991',1), + ('30/11/1991',1.000933333), + ('31/12/1991',1.0062), + ('31/01/1992',1.0112), + ('29/02/1992',1.0158), + ('31/03/1992',1.020733333), + ('30/04/1992',1.026133333), + ('31/05/1992',1.030266667), + ('30/06/1992',1.034466667), + ('31/07/1992',1.038733333), + ('31/08/1992',1.0422), + ('30/09/1992',1.045533333), + ('31/10/1992',1.049866667), + ('30/11/1992',1.054733333), + ('31/12/1992',1.061), + ), + ] + + data[0] = preprocessData(data[0]) + + lp = LinePlot() + + lp.x = 50 + lp.y = 50 + lp.height = 125 + lp.width = 300 + lp.data = data + lp.joinedLines = 1 + lp.lines.symbol = makeMarker('FilledDiamond') + lp.strokeColor = colors.black + + start = mktime(mkTimeTuple('25/11/1991')) + t0 = mktime(mkTimeTuple('30/11/1991')) + t1 = mktime(mkTimeTuple('31/12/1991')) + t2 = mktime(mkTimeTuple('31/03/1992')) + t3 = mktime(mkTimeTuple('30/06/1992')) + t4 = mktime(mkTimeTuple('30/09/1992')) + end = mktime(mkTimeTuple('31/12/1992')) + lp.xValueAxis.valueMin = start + lp.xValueAxis.valueMax = end + lp.xValueAxis.valueSteps = [start, t0, t1, t2, t3, t4, end] + lp.xValueAxis.labelTextFormat = seconds2str + lp.xValueAxis.labels[1].dy = -20 + lp.xValueAxis.labels[2].dy = -35 + + lp.yValueAxis.labelTextFormat = '%4.2f' + lp.yValueAxis.valueMin = 100 + lp.yValueAxis.valueMax = 110 + lp.yValueAxis.valueStep = 2 + + drawing.add(lp) + + return drawing diff --git a/bin/reportlab/graphics/charts/markers.py b/bin/reportlab/graphics/charts/markers.py new file mode 100644 index 00000000000..cb3493cb36b --- /dev/null +++ b/bin/reportlab/graphics/charts/markers.py @@ -0,0 +1,81 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/markers.py +""" +This modules defines a collection of markers used in charts. + +The make* functions return a simple shape or a widget as for +the smiley. +""" +__version__=''' $Id: markers.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' +from reportlab.lib import colors +from reportlab.graphics.shapes import Rect, Line, Circle, Polygon +from reportlab.graphics.widgets.signsandsymbols import SmileyFace + + +def makeEmptySquare(x, y, size, color): + "Make an empty square marker." + + d = size/2.0 + rect = Rect(x-d, y-d, 2*d, 2*d) + rect.strokeColor = color + rect.fillColor = None + + return rect + + +def makeFilledSquare(x, y, size, color): + "Make a filled square marker." + + d = size/2.0 + rect = Rect(x-d, y-d, 2*d, 2*d) + rect.strokeColor = color + rect.fillColor = color + + return rect + + +def makeFilledDiamond(x, y, size, color): + "Make a filled diamond marker." + + d = size/2.0 + poly = Polygon((x-d,y, x,y+d, x+d,y, x,y-d)) + poly.strokeColor = color + poly.fillColor = color + + return poly + + +def makeEmptyCircle(x, y, size, color): + "Make a hollow circle marker." + + d = size/2.0 + circle = Circle(x, y, d) + circle.strokeColor = color + circle.fillColor = colors.white + + return circle + + +def makeFilledCircle(x, y, size, color): + "Make a hollow circle marker." + + d = size/2.0 + circle = Circle(x, y, d) + circle.strokeColor = color + circle.fillColor = color + + return circle + + +def makeSmiley(x, y, size, color): + "Make a smiley marker." + + d = size + s = SmileyFace() + s.fillColor = color + s.x = x-d + s.y = y-d + s.size = d*2 + + return s \ No newline at end of file diff --git a/bin/reportlab/graphics/charts/piecharts.py b/bin/reportlab/graphics/charts/piecharts.py new file mode 100644 index 00000000000..f8646086b8e --- /dev/null +++ b/bin/reportlab/graphics/charts/piecharts.py @@ -0,0 +1,1279 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/piecharts.py +# experimental pie chart script. Two types of pie - one is a monolithic +#widget with all top-level properties, the other delegates most stuff to +#a wedges collection whic lets you customize the group or every individual +#wedge. + +"""Basic Pie Chart class. + +This permits you to customize and pop out individual wedges; +supports elliptical and circular pies. +""" +__version__=''' $Id: piecharts.py 2743 2005-12-12 15:51:29Z rgbecker $ ''' + +import copy +from math import sin, cos, pi + +from reportlab.lib import colors +from reportlab.lib.validators import isColor, isNumber, isListOfNumbersOrNone,\ + isListOfNumbers, isColorOrNone, isString,\ + isListOfStringsOrNone, OneOf, SequenceOf,\ + isBoolean, isListOfColors, isNumberOrNone,\ + isNoneOrListOfNoneOrStrings, isTextAnchor,\ + isNoneOrListOfNoneOrNumbers, isBoxAnchor,\ + isStringOrNone, NoneOr +from reportlab.graphics.widgets.markers import uSymbol2Symbol, isSymbol +from reportlab.lib.attrmap import * +from reportlab.pdfgen.canvas import Canvas +from reportlab.graphics.shapes import Group, Drawing, Ellipse, Wedge, String, STATE_DEFAULTS, ArcPath, Polygon, Rect, PolyLine +from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder +from reportlab.graphics.charts.areas import PlotArea +from textlabels import Label + +_ANGLE2BOXANCHOR={0:'w', 45:'sw', 90:'s', 135:'se', 180:'e', 225:'ne', 270:'n', 315: 'nw', -45: 'nw'} +_ANGLE2RBOXANCHOR={0:'e', 45:'ne', 90:'n', 135:'nw', 180:'w', 225:'sw', 270:'s', 315: 'se', -45: 'se'} +class WedgeLabel(Label): + def _checkDXY(self,ba): + pass + def _getBoxAnchor(self): + na = (int((self._pmv%360)/45.)*45)%360 + if not (na % 90): # we have a right angle case + da = (self._pmv - na) % 360 + if abs(da)>5: + na = na + (da>0 and 45 or -45) + ba = (getattr(self,'_anti',None) and _ANGLE2RBOXANCHOR or _ANGLE2BOXANCHOR)[na] + self._checkDXY(ba) + return ba + +class WedgeProperties(PropHolder): + """This holds descriptive information about the wedges in a pie chart. + + It is not to be confused with the 'wedge 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 Wedge object for you with its + format method. + """ + _attrMap = AttrMap( + strokeWidth = AttrMapValue(isNumber), + fillColor = AttrMapValue(isColorOrNone), + strokeColor = AttrMapValue(isColorOrNone), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone), + popout = AttrMapValue(isNumber), + fontName = AttrMapValue(isString), + fontSize = AttrMapValue(isNumber), + fontColor = AttrMapValue(isColorOrNone), + labelRadius = AttrMapValue(isNumber), + label_dx = AttrMapValue(isNumber), + label_dy = AttrMapValue(isNumber), + label_angle = AttrMapValue(isNumber), + label_boxAnchor = AttrMapValue(isBoxAnchor), + label_boxStrokeColor = AttrMapValue(isColorOrNone), + label_boxStrokeWidth = AttrMapValue(isNumber), + label_boxFillColor = AttrMapValue(isColorOrNone), + label_strokeColor = AttrMapValue(isColorOrNone), + label_strokeWidth = AttrMapValue(isNumber), + label_text = AttrMapValue(isStringOrNone), + label_leading = AttrMapValue(isNumberOrNone), + label_width = AttrMapValue(isNumberOrNone), + label_maxWidth = AttrMapValue(isNumberOrNone), + label_height = AttrMapValue(isNumberOrNone), + label_textAnchor = AttrMapValue(isTextAnchor), + label_visible = AttrMapValue(isBoolean,desc="True if the label is to be drawn"), + label_topPadding = AttrMapValue(isNumber,'padding at top of box'), + label_leftPadding = AttrMapValue(isNumber,'padding at left of box'), + label_rightPadding = AttrMapValue(isNumber,'padding at right of box'), + label_bottomPadding = AttrMapValue(isNumber,'padding at bottom of box'), + label_pointer_strokeColor = AttrMapValue(isColorOrNone,desc='Color of indicator line'), + label_pointer_strokeWidth = AttrMapValue(isNumber,desc='StrokeWidth of indicator line'), + label_pointer_elbowLength = AttrMapValue(isNumber,desc='length of final indicator line segment'), + label_pointer_edgePad = AttrMapValue(isNumber,desc='pad between pointer label and box'), + label_pointer_piePad = AttrMapValue(isNumber,desc='pad between pointer label and pie'), + swatchMarker = AttrMapValue(NoneOr(isSymbol), desc="None or makeMarker('Diamond') ..."), + ) + + def __init__(self): + self.strokeWidth = 0 + self.fillColor = None + self.strokeColor = STATE_DEFAULTS["strokeColor"] + self.strokeDashArray = STATE_DEFAULTS["strokeDashArray"] + self.popout = 0 + self.fontName = STATE_DEFAULTS["fontName"] + self.fontSize = STATE_DEFAULTS["fontSize"] + self.fontColor = STATE_DEFAULTS["fillColor"] + self.labelRadius = 1.2 + self.label_dx = self.label_dy = self.label_angle = 0 + self.label_text = None + self.label_topPadding = self.label_leftPadding = self.label_rightPadding = self.label_bottomPadding = 0 + self.label_boxAnchor = 'c' + self.label_boxStrokeColor = None #boxStroke + self.label_boxStrokeWidth = 0.5 #boxStrokeWidth + self.label_boxFillColor = None + self.label_strokeColor = None + self.label_strokeWidth = 0.1 + self.label_leading = self.label_width = self.label_maxWidth = self.label_height = None + self.label_textAnchor = 'start' + self.label_visible = 1 + self.label_pointer_strokeColor = colors.black + self.label_pointer_strokeWidth = 0.5 + self.label_pointer_elbowLength = 3 + self.label_pointer_edgePad = 2 + self.label_pointer_piePad = 3 + +def _addWedgeLabel(self,text,add,angle,labelX,labelY,wedgeStyle,labelClass=WedgeLabel): + # now draw a label + if self.simpleLabels: + theLabel = String(labelX, labelY, text) + theLabel.textAnchor = "middle" + theLabel._pmv = angle + else: + theLabel = labelClass() + theLabel._pmv = angle + theLabel.x = labelX + theLabel.y = labelY + theLabel.dx = wedgeStyle.label_dx + theLabel.dy = wedgeStyle.label_dy + theLabel.angle = wedgeStyle.label_angle + theLabel.boxAnchor = wedgeStyle.label_boxAnchor + theLabel.boxStrokeColor = wedgeStyle.label_boxStrokeColor + theLabel.boxStrokeWidth = wedgeStyle.label_boxStrokeWidth + theLabel.boxFillColor = wedgeStyle.label_boxFillColor + theLabel.strokeColor = wedgeStyle.label_strokeColor + theLabel.strokeWidth = wedgeStyle.label_strokeWidth + _text = wedgeStyle.label_text + if _text is None: _text = text + theLabel._text = _text + theLabel.leading = wedgeStyle.label_leading + theLabel.width = wedgeStyle.label_width + theLabel.maxWidth = wedgeStyle.label_maxWidth + theLabel.height = wedgeStyle.label_height + theLabel.textAnchor = wedgeStyle.label_textAnchor + theLabel.visible = wedgeStyle.label_visible + theLabel.topPadding = wedgeStyle.label_topPadding + theLabel.leftPadding = wedgeStyle.label_leftPadding + theLabel.rightPadding = wedgeStyle.label_rightPadding + theLabel.bottomPadding = wedgeStyle.label_bottomPadding + theLabel.fontSize = wedgeStyle.fontSize + theLabel.fontName = wedgeStyle.fontName + theLabel.fillColor = wedgeStyle.fontColor + add(theLabel) + +def _fixLabels(labels,n): + if labels is None: + labels = [''] * n + else: + i = n-len(labels) + if i>0: labels = labels + ['']*i + return labels + +class AbstractPieChart(PlotArea): + + def makeSwatchSample(self, rowNo, x, y, width, height): + baseStyle = self.slices + 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',None)) + swatchMarker = getattr(style, 'swatchMarker', getattr(baseStyle, 'swatchMarker',None)) + if swatchMarker: + return uSymbol2Symbol(swatchMarker,x+width/2.,y+height/2.,fillColor) + return Rect(x,y,width,height,strokeWidth=strokeWidth,strokeColor=strokeColor, + strokeDashArray=strokeDashArray,fillColor=fillColor) + + def getSeriesName(self,i,default=None): + '''return series name i or default''' + try: + text = str(self.labels[i]) + except: + text = default + if not self.simpleLabels: + _text = getattr(self.slices[i],'label_text','') + if _text is not None: text = _text + return text + +def boundsOverlap(P,Q): + return not(P[0]>Q[2]-1e-2 or Q[0]>P[2]-1e-2 or P[1]>Q[3]-1e-2 or Q[1]>P[3]-1e-2) + +def _findOverlapRun(B,i,wrap): + '''find overlap run containing B[i]''' + n = len(B) + R = [i] + while 1: + i = R[-1] + j = (i+1)%n + if j in R or not boundsOverlap(B[i],B[j]): break + R.append(j) + while 1: + i = R[0] + j = (i-1)%n + if j in R or not boundsOverlap(B[i],B[j]): break + R.insert(0,j) + return R + +def findOverlapRun(B,wrap=1): + '''determine a set of overlaps in bounding boxes B or return None''' + n = len(B) + if n>1: + for i in xrange(n-1): + R = _findOverlapRun(B,i,wrap) + if len(R)>1: return R + return None + +def fixLabelOverlaps(L): + nL = len(L) + if nL<2: return + B = [l._origdata['bounds'] for l in L] + OK = 1 + RP = [] + iter = 0 + mult = 1. + + while iter<30: + R = findOverlapRun(B) + if not R: break + nR = len(R) + if nR==nL: break + if not [r for r in RP if r in R]: + mult = 1.0 + da = 0 + r0 = R[0] + rL = R[-1] + bi = B[r0] + taa = aa = _360(L[r0]._pmv) + for r in R[1:]: + b = B[r] + da = max(da,min(b[3]-bi[1],bi[3]-b[1])) + bi = b + aa += L[r]._pmv + aa = aa/float(nR) + utaa = abs(L[rL]._pmv-taa) + ntaa = _360(utaa) + da *= mult*(nR-1)/ntaa + + for r in R: + l = L[r] + orig = l._origdata + angle = l._pmv = _360(l._pmv+da*(_360(l._pmv)-aa)) + rad = angle/_180_pi + l.x = orig['cx'] + orig['rx']*cos(rad) + l.y = orig['cy'] + orig['ry']*sin(rad) + B[r] = l.getBounds() + RP = R + mult *= 1.05 + iter += 1 + +def intervalIntersection(A,B): + x,y = max(min(A),min(B)),min(max(A),max(B)) + if x>=y: return None + return x,y + +def _makeSideArcDefs(sa,direction): + sa %= 360 + if 90<=sa<270: + if direction=='clockwise': + a = (0,90,sa),(1,-90,90),(0,-360+sa,-90) + else: + a = (0,sa,270),(1,270,450),(0,450,360+sa) + else: + offs = sa>=270 and 360 or 0 + if direction=='clockwise': + a = (1,offs-90,sa),(0,offs-270,offs-90),(1,-360+sa,offs-270) + else: + a = (1,sa,offs+90),(0,offs+90,offs+270),(1,offs+270,360+sa) + return tuple([a for a in a if a[1]1: a.sort(lambda x,y: cmp(y[1]-y[0],x[1]-x[0])) + return a[0] + +def _fPLSide(l,width,side=None): + data = l._origdata + if side is None: + li = data['li'] + ri = data['ri'] + if li is None: + side = 1 + i = ri + elif ri is None: + side = 0 + i = li + elif li[1]-li[0]>ri[1]-ri[0]: + side = 0 + i = li + else: + side = 1 + i = ri + w = data['width'] + edgePad = data['edgePad'] + if not side: #on left + l._pmv = 180 + l.x = edgePad+w + i = data['li'] + else: + l._pmv = 0 + l.x = width - w - edgePad + i = data['ri'] + mid = data['mid'] = (i[0]+i[1])*0.5 + data['smid'] = sin(mid/_180_pi) + data['cmid'] = cos(mid/_180_pi) + data['side'] = side + return side,w + +def _fPLCF(a,b): + return cmp(b._origdata['smid'],a._origdata['smid']) + +def _arcCF(a,b): + return cmp(a[1],b[1]) + +def _fixPointerLabels(n,L,x,y,width,height,side=None): + LR = [],[] + mlr = [0,0] + for l in L: + i,w = _fPLSide(l,width,side) + LR[i].append(l) + mlr[i] = max(w,mlr[i]) + mul = 1 + G = n*[None] + mel = 0 + hh = height*0.5 + yhh = y+hh + m = max(mlr) + for i in (0,1): + T = LR[i] + if T: + B = [] + aB = B.append + S = [] + aS = S.append + T.sort(_fPLCF) + p = 0 + yh = y+height + for l in T: + data = l._origdata + inc = x+mul*(m-data['width']) + l.x += inc + G[data['index']] = l + ly = yhh+data['smid']*hh + b = data['bounds'] + b2 = (b[3]-b[1])*0.5 + if ly+b2>yh: ly = yh-b2 + if ly-b2sFree: break + yh = B[j0][3]+sAbove*sNeed/sFree + for r in R: + l = T[r] + data = l._origdata + b = data['bounds'] + b2 = (b[3]-b[1])*0.5 + yh -= 0.5 + ly = l.y = yh-b2 + B[r] = data['bounds'] = (b[0],ly-b2,b[2],yh) + yh = ly - b2 - 0.5 + mlr[i] = m+p + mul = -1 + return G, mlr[0], mlr[1], mel + +class Pie(AbstractPieChart): + _attrMap = AttrMap(BASE=AbstractPieChart, + data = AttrMapValue(isListOfNumbers, desc='list of numbers defining wedge 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 wedge descriptor objects"), + simpleLabels = AttrMapValue(isBoolean, desc="If true(default) use String not super duper WedgeLabel"), + other_threshold = AttrMapValue(isNumber, desc='A value for doing threshholding, not used yet.'), + checkLabelOverlap = AttrMapValue(isBoolean, desc="If true check and attempt to fix standard label overlaps(default off)"), + pointerLabelMode = AttrMapValue(OneOf(None,'LeftRight','LeftAndRight'), desc=""), + sameRadii = AttrMapValue(isBoolean, desc="If true make x/y radii the same(default off)"), + orderMode = AttrMapValue(OneOf('fixed','alternate')), + xradius = AttrMapValue(isNumberOrNone, desc="X direction Radius"), + yradius = AttrMapValue(isNumberOrNone, desc="Y direction Radius"), + ) + other_threshold=None + + def __init__(self,**kwd): + PlotArea.__init__(self) + self.x = 0 + self.y = 0 + self.width = 100 + self.height = 100 + self.data = [1,2.3,1.7,4.2] + self.labels = None # or list of strings + self.startAngle = 90 + self.direction = "clockwise" + self.simpleLabels = 1 + self.checkLabelOverlap = 0 + self.pointerLabelMode = None + self.sameRadii = False + self.orderMode = 'fixed' + self.xradius = self.yradius = None + + self.slices = TypedPropertyCollection(WedgeProperties) + 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) + + pc = Pie() + pc.x = 50 + pc.y = 10 + pc.width = 100 + pc.height = 80 + pc.data = [10,20,30,40,50,60] + pc.labels = ['a','b','c','d','e','f'] + + pc.slices.strokeWidth=0.5 + pc.slices[3].popout = 10 + pc.slices[3].strokeWidth = 2 + pc.slices[3].strokeDashArray = [2,2] + pc.slices[3].labelRadius = 1.75 + pc.slices[3].fontColor = colors.red + pc.slices[0].fillColor = colors.darkcyan + pc.slices[1].fillColor = colors.blueviolet + pc.slices[2].fillColor = colors.blue + pc.slices[3].fillColor = colors.cyan + pc.slices[4].fillColor = colors.aquamarine + pc.slices[5].fillColor = colors.cadetblue + pc.slices[6].fillColor = colors.lightcoral + + d.add(pc) + return d + + def makePointerLabels(self,angles,plMode): + class PL: + def __init__(self,centerx,centery,xradius,yradius,data,lu=0,ru=0): + self.centerx = centerx + self.centery = centery + self.xradius = xradius + self.yradius = yradius + self.data = data + self.lu = lu + self.ru = ru + + labelX = self.width-2 + labelY = self.height + n = nr = nl = maxW = sumH = 0 + styleCount = len(self.slices) + L=[] + L_add = L.append + refArcs = _makeSideArcDefs(self.startAngle,self.direction) + for i, A in angles: + if A[1] is None: continue + sn = self.getSeriesName(i,'') + if not sn: continue + n += 1 + style = self.slices[i%styleCount] + _addWedgeLabel(self,sn,L_add,180,labelX,labelY,style,labelClass=WedgeLabel) + l = L[-1] + b = l.getBounds() + w = b[2]-b[0] + h = b[3]-b[1] + ri = [(a[0],intervalIntersection(A,(a[1],a[2]))) for a in refArcs] + li = _findLargestArc(ri,0) + ri = _findLargestArc(ri,1) + if li and ri: + if plMode=='LeftAndRight': + if li[1]-li[0]ri[1]-ri[0]: + ri = None + if ri: nr += 1 + if li: nl += 1 + l._origdata = dict(bounds=b,width=w,height=h,li=li,ri=ri,index=i,edgePad=style.label_pointer_edgePad,piePad=style.label_pointer_piePad,elbowLength=style.label_pointer_elbowLength) + maxW = max(w,maxW) + sumH += h+2 + + if not n: #we have no labels + xradius = self.width*0.5 + yradius = self.height*0.5 + centerx = self.x+xradius + centery = self.y+yradius + if self.xradius: xradius = self.xradius + if self.yradius: yradius = self.yradius + if self.sameRadii: xradius=yradius=min(xradius,yradius) + return PL(centerx,centery,xradius,yradius,[]) + + aonR = nr==n + if sumH=1e-8 and map(lambda x,f=360./sum: f*x, data) or len(data)*[0] + + def makeAngles(self): + startAngle = self.startAngle % 360 + whichWay = self.direction == "clockwise" and -1 or 1 + D = [a for a in enumerate(self.normalizeData())] + if self.orderMode=='alternate': + W = [a for a in D if abs(a[1])>=1e-5] + W.sort(_arcCF) + T = [[],[]] + i = 0 + while W: + if i<2: + a = W.pop(0) + else: + a = W.pop(-1) + T[i%2].append(a) + i += 1 + i %= 4 + T[1].reverse() + D = T[0]+T[1] + [a for a in D if abs(a[1])<1e-5] + A = [] + a = A.append + for i, angle in D: + endAngle = (startAngle + (angle * whichWay)) + if abs(angle)>=1e-5: + if startAngle >= endAngle: + aa = endAngle,startAngle + else: + aa = startAngle,endAngle + else: + aa = startAngle, None + startAngle = endAngle + a((i,aa)) + return A + + def makeWedges(self): + angles = self.makeAngles() + n = len(angles) + labels = _fixLabels(self.labels,n) + + self._seriesCount = n + styleCount = len(self.slices) + + plMode = self.pointerLabelMode + if plMode: + checkLabelOverlap = False + PL=self.makePointerLabels(angles,plMode) + xradius = PL.xradius + yradius = PL.yradius + centerx = PL.centerx + centery = PL.centery + PL_data = PL.data + gSN = lambda i: '' + else: + xradius = self.width*0.5 + yradius = self.height*0.5 + centerx = self.x + xradius + centery = self.y + yradius + if self.xradius: xradius = self.xradius + if self.yradius: yradius = self.yradius + if self.sameRadii: xradius=yradius=min(xradius,yradius) + checkLabelOverlap = self.checkLabelOverlap + gSN = lambda i: self.getSeriesName(i,'') + + g = Group() + g_add = g.add + if checkLabelOverlap: + L = [] + L_add = L.append + else: + L_add = g_add + + for i,(a1,a2) in angles: + if a2 is None: continue + #if we didn't use %stylecount here we'd end up with the later wedges + #all having the default style + wedgeStyle = self.slices[i%styleCount] + + # is it a popout? + cx, cy = centerx, centery + text = gSN(i) + popout = wedgeStyle.popout + if text or popout: + averageAngle = (a1+a2)/2.0 + aveAngleRadians = averageAngle/_180_pi + cosAA = cos(aveAngleRadians) + sinAA = sin(aveAngleRadians) + if popout: + # pop out the wedge + cx = centerx + popout*cosAA + cy = centery + popout*sinAA + + if n > 1: + theWedge = Wedge(cx, cy, xradius, a1, a2, yradius=yradius) + elif n==1: + theWedge = Ellipse(cx, cy, xradius, yradius) + + theWedge.fillColor = wedgeStyle.fillColor + theWedge.strokeColor = wedgeStyle.strokeColor + theWedge.strokeWidth = wedgeStyle.strokeWidth + theWedge.strokeDashArray = wedgeStyle.strokeDashArray + + g_add(theWedge) + if text: + labelRadius = wedgeStyle.labelRadius + rx = xradius*labelRadius + ry = yradius*labelRadius + labelX = cx + rx*cosAA + labelY = cy + ry*sinAA + _addWedgeLabel(self,text,L_add,averageAngle,labelX,labelY,wedgeStyle) + if checkLabelOverlap: + l = L[-1] + l._origdata = { 'x': labelX, 'y':labelY, 'angle': averageAngle, + 'rx': rx, 'ry':ry, 'cx':cx, 'cy':cy, + 'bounds': l.getBounds(), + } + elif plMode and PL_data: + l = PL_data[i] + if l: + data = l._origdata + sinM = data['smid'] + cosM = data['cmid'] + lX = cx + xradius*cosM + lY = cy + yradius*sinM + lpel = wedgeStyle.label_pointer_elbowLength + lXi = lX + lpel*cosM + lYi = lY + lpel*sinM + L_add(PolyLine((lX,lY,lXi,lYi,l.x,l.y), + strokeWidth=wedgeStyle.label_pointer_strokeWidth, + strokeColor=wedgeStyle.label_pointer_strokeColor)) + L_add(l) + + if checkLabelOverlap: + fixLabelOverlaps(L) + map(g_add,L) + + return g + + def draw(self): + G = self.makeBackground() + w = self.makeWedges() + if G: return Group(G,w) + return w + +class LegendedPie(Pie): + """Pie with a two part legend (one editable with swatches, one hidden without swatches).""" + + _attrMap = AttrMap(BASE=Pie, + drawLegend = AttrMapValue(isBoolean, desc="If true then create and draw legend"), + legend1 = AttrMapValue(None, desc="Handle to legend for pie"), + legendNumberFormat = AttrMapValue(None, desc="Formatting routine for number on right hand side of legend."), + legendNumberOffset = AttrMapValue(isNumber, desc="Horizontal space between legend and numbers on r/hand side"), + pieAndLegend_colors = AttrMapValue(isListOfColors, desc="Colours used for both swatches and pie"), + legend_names = AttrMapValue(isNoneOrListOfNoneOrStrings, desc="Names used in legend (or None)"), + legend_data = AttrMapValue(isNoneOrListOfNoneOrNumbers, desc="Numbers used on r/hand side of legend (or None)"), + 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'), + ) + + def __init__(self): + Pie.__init__(self) + self.x = 0 + self.y = 0 + self.height = 100 + self.width = 100 + self.data = [38.4, 20.7, 18.9, 15.4, 6.6] + self.labels = None + self.direction = 'clockwise' + PCMYKColor, black = colors.PCMYKColor, colors.black + self.pieAndLegend_colors = [PCMYKColor(11,11,72,0,spotName='PANTONE 458 CV'), + PCMYKColor(100,65,0,30,spotName='PANTONE 288 CV'), + PCMYKColor(11,11,72,0,spotName='PANTONE 458 CV',density=75), + PCMYKColor(100,65,0,30,spotName='PANTONE 288 CV',density=75), + PCMYKColor(11,11,72,0,spotName='PANTONE 458 CV',density=50), + PCMYKColor(100,65,0,30,spotName='PANTONE 288 CV',density=50)] + + #Allows us up to six 'wedges' to be coloured + self.slices[0].fillColor=self.pieAndLegend_colors[0] + self.slices[1].fillColor=self.pieAndLegend_colors[1] + self.slices[2].fillColor=self.pieAndLegend_colors[2] + self.slices[3].fillColor=self.pieAndLegend_colors[3] + self.slices[4].fillColor=self.pieAndLegend_colors[4] + self.slices[5].fillColor=self.pieAndLegend_colors[5] + + self.slices.strokeWidth = 0.75 + self.slices.strokeColor = black + + legendOffset = 17 + self.legendNumberOffset = 51 + self.legendNumberFormat = '%.1f%%' + self.legend_data = self.data + + #set up the legends + from reportlab.graphics.charts.legends import Legend + self.legend1 = Legend() + self.legend1.x = self.width+legendOffset + self.legend1.y = self.height + self.legend1.deltax = 5.67 + self.legend1.deltay = 14.17 + self.legend1.dxTextSpace = 11.39 + self.legend1.dx = 5.67 + self.legend1.dy = 5.67 + self.legend1.columnMaximum = 7 + self.legend1.alignment = 'right' + self.legend_names = ['AAA:','AA:','A:','BBB:','NR:'] + for f in range(0,len(self.data)): + self.legend1.colorNamePairs.append((self.pieAndLegend_colors[f], self.legend_names[f])) + self.legend1.fontName = "Helvetica-Bold" + self.legend1.fontSize = 6 + self.legend1.strokeColor = black + self.legend1.strokeWidth = 0.5 + + self._legend2 = Legend() + self._legend2.dxTextSpace = 0 + self._legend2.dx = 0 + self._legend2.alignment = 'right' + self._legend2.fontName = "Helvetica-Oblique" + self._legend2.fontSize = 6 + self._legend2.strokeColor = self.legend1.strokeColor + + self.leftPadding = 5 + self.rightPadding = 5 + self.topPadding = 5 + self.bottomPadding = 5 + self.drawLegend = 1 + + def draw(self): + if self.drawLegend: + self.legend1.colorNamePairs = [] + self._legend2.colorNamePairs = [] + for f in range(0,len(self.data)): + if self.legend_names == None: + self.slices[f].fillColor = self.pieAndLegend_colors[f] + self.legend1.colorNamePairs.append((self.pieAndLegend_colors[f], None)) + else: + try: + self.slices[f].fillColor = self.pieAndLegend_colors[f] + self.legend1.colorNamePairs.append((self.pieAndLegend_colors[f], self.legend_names[f])) + except IndexError: + self.slices[f].fillColor = self.pieAndLegend_colors[f%len(self.pieAndLegend_colors)] + self.legend1.colorNamePairs.append((self.pieAndLegend_colors[f%len(self.pieAndLegend_colors)], self.legend_names[f])) + if self.legend_data != None: + ldf = self.legend_data[f] + lNF = self.legendNumberFormat + from types import StringType + if ldf is None or lNF is None: + pass + elif type(lNF) is StringType: + ldf = lNF % ldf + elif callable(lNF): + ldf = lNF(ldf) + else: + p = self.legend_names[f] + if self.legend_data != None: + ldf = self.legend_data[f] + lNF = self.legendNumberFormat + if ldf is None or lNF is None: + pass + elif type(lNF) is StringType: + ldf = lNF % ldf + elif callable(lNF): + ldf = lNF(ldf) + else: + msg = "Unknown formatter type %s, expected string or function" % self.legendNumberFormat + raise Exception, msg + self._legend2.colorNamePairs.append((None,ldf)) + p = Pie.draw(self) + if self.drawLegend: + p.add(self.legend1) + #hide from user - keeps both sides lined up! + self._legend2.x = self.legend1.x+self.legendNumberOffset + self._legend2.y = self.legend1.y + self._legend2.deltax = self.legend1.deltax + self._legend2.deltay = self.legend1.deltay + self._legend2.dy = self.legend1.dy + self._legend2.columnMaximum = self.legend1.columnMaximum + p.add(self._legend2) + p.shift(self.leftPadding, self.bottomPadding) + return p + + def _getDrawingDimensions(self): + tx = self.rightPadding + if self.drawLegend: + tx = tx+self.legend1.x+self.legendNumberOffset #self._legend2.x + tx = tx + self._legend2._calculateMaxWidth(self._legend2.colorNamePairs) + ty = self.bottomPadding+self.height+self.topPadding + 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 + +from utils3d import _getShaded, _2rad, _360, _pi_2, _2pi, _180_pi +class Wedge3dProperties(PropHolder): + """This holds descriptive information about the wedges in a pie chart. + + It is not to be confused with the 'wedge 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 Wedge object for you with its + format method. + """ + _attrMap = AttrMap( + fillColor = AttrMapValue(isColorOrNone), + fillColorShaded = AttrMapValue(isColorOrNone), + fontColor = AttrMapValue(isColorOrNone), + fontName = AttrMapValue(isString), + fontSize = AttrMapValue(isNumber), + label_angle = AttrMapValue(isNumber), + label_bottomPadding = AttrMapValue(isNumber,'padding at bottom of box'), + label_boxAnchor = AttrMapValue(isBoxAnchor), + label_boxFillColor = AttrMapValue(isColorOrNone), + label_boxStrokeColor = AttrMapValue(isColorOrNone), + label_boxStrokeWidth = AttrMapValue(isNumber), + label_dx = AttrMapValue(isNumber), + label_dy = AttrMapValue(isNumber), + label_height = AttrMapValue(isNumberOrNone), + label_leading = AttrMapValue(isNumberOrNone), + label_leftPadding = AttrMapValue(isNumber,'padding at left of box'), + label_maxWidth = AttrMapValue(isNumberOrNone), + label_rightPadding = AttrMapValue(isNumber,'padding at right of box'), + label_strokeColor = AttrMapValue(isColorOrNone), + label_strokeWidth = AttrMapValue(isNumber), + label_text = AttrMapValue(isStringOrNone), + label_textAnchor = AttrMapValue(isTextAnchor), + label_topPadding = AttrMapValue(isNumber,'padding at top of box'), + label_visible = AttrMapValue(isBoolean,desc="True if the label is to be drawn"), + label_width = AttrMapValue(isNumberOrNone), + labelRadius = AttrMapValue(isNumber), + popout = AttrMapValue(isNumber), + shading = AttrMapValue(isNumber), + strokeColor = AttrMapValue(isColorOrNone), + strokeColorShaded = AttrMapValue(isColorOrNone), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone), + strokeWidth = AttrMapValue(isNumber), + visible = AttrMapValue(isBoolean,'set to false to skip displaying'), + ) + + def __init__(self): + self.strokeWidth = 0 + self.shading = 0.3 + self.visible = 1 + self.strokeColorShaded = self.fillColorShaded = self.fillColor = None + self.strokeColor = STATE_DEFAULTS["strokeColor"] + self.strokeDashArray = STATE_DEFAULTS["strokeDashArray"] + self.popout = 0 + self.fontName = STATE_DEFAULTS["fontName"] + self.fontSize = STATE_DEFAULTS["fontSize"] + self.fontColor = STATE_DEFAULTS["fillColor"] + self.labelRadius = 1.2 + self.label_dx = self.label_dy = self.label_angle = 0 + self.label_text = None + self.label_topPadding = self.label_leftPadding = self.label_rightPadding = self.label_bottomPadding = 0 + self.label_boxAnchor = 'c' + self.label_boxStrokeColor = None #boxStroke + self.label_boxStrokeWidth = 0.5 #boxStrokeWidth + self.label_boxFillColor = None + self.label_strokeColor = None + self.label_strokeWidth = 0.1 + self.label_leading = self.label_width = self.label_maxWidth = self.label_height = None + self.label_textAnchor = 'start' + self.label_visible = 1 + +class _SL3D: + def __init__(self,lo,hi): + if lo<0: + lo += 360 + hi += 360 + self.lo = lo + self.hi = hi + self.mid = (lo+hi)*0.5 + + def __str__(self): + return '_SL3D(%.2f,%.2f)' % (self.lo,self.hi) + +_270r = _2rad(270) +class Pie3d(Pie): + _attrMap = AttrMap(BASE=Pie, + perspective = AttrMapValue(isNumber, desc='A flattening parameter.'), + depth_3d = AttrMapValue(isNumber, desc='depth of the pie.'), + angle_3d = AttrMapValue(isNumber, desc='The view angle.'), + ) + perspective = 70 + depth_3d = 25 + angle_3d = 180 + + def _popout(self,i): + return self.slices[i].popout or 0 + + def CX(self, i,d ): + return self._cx+(d and self._xdepth_3d or 0)+self._popout(i)*cos(_2rad(self._sl3d[i].mid)) + def CY(self,i,d): + return self._cy+(d and self._ydepth_3d or 0)+self._popout(i)*sin(_2rad(self._sl3d[i].mid)) + def OX(self,i,o,d): + return self.CX(i,d)+self._radiusx*cos(_2rad(o)) + def OY(self,i,o,d): + return self.CY(i,d)+self._radiusy*sin(_2rad(o)) + + def rad_dist(self,a): + _3dva = self._3dva + return min(abs(a-_3dva),abs(a-_3dva+360)) + + def __init__(self): + self.x = 0 + self.y = 0 + self.width = 300 + self.height = 200 + self.data = [12.50,20.10,2.00,22.00,5.00,18.00,13.00] + self.labels = None # or list of strings + self.startAngle = 90 + self.direction = "clockwise" + self.simpleLabels = 1 + self.slices = TypedPropertyCollection(Wedge3dProperties) + 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.azure + self.slices[5].fillColor = colors.crimson + self.slices[6].fillColor = colors.darkviolet + self.checkLabelOverlap = 0 + + def _fillSide(self,L,i,angle,strokeColor,strokeWidth,fillColor): + rd = self.rad_dist(angle) + if rd0: angle0, angle1 = angle1, angle0 + _sl3d.append(_SL3D(angle0,angle1)) + + labels = _fixLabels(self.labels,n) + a0 = _3d_angle + a1 = _3d_angle+180 + T = [] + S = [] + L = [] + + class WedgeLabel3d(WedgeLabel): + _ydepth_3d = self._ydepth_3d + def _checkDXY(self,ba): + if ba[0]=='n': + if not hasattr(self,'_ody'): + self._ody = self.dy + self.dy = -self._ody + self._ydepth_3d + + checkLabelOverlap = self.checkLabelOverlap + + for i in xrange(n): + style = slices[i] + if not style.visible: continue + sl = _sl3d[i] + lo = angle0 = sl.lo + hi = angle1 = sl.hi + if abs(hi-lo)<=1e-7: continue + fillColor = _getShaded(style.fillColor,style.fillColorShaded,style.shading) + strokeColor = _getShaded(style.strokeColor,style.strokeColorShaded,style.shading) or fillColor + strokeWidth = style.strokeWidth + cx0 = CX(i,0) + cy0 = CY(i,0) + cx1 = CX(i,1) + cy1 = CY(i,1) + #background shaded pie bottom + g.add(Wedge(cx1,cy1,radiusx, lo, hi,yradius=radiusy, + strokeColor=strokeColor,strokeWidth=strokeWidth,fillColor=fillColor, + strokeLineJoin=1)) + #connect to top + if lo < a0 < hi: angle0 = a0 + if lo < a1 < hi: angle1 = a1 + if 1: + p = ArcPath(strokeColor=strokeColor, fillColor=fillColor,strokeWidth=strokeWidth,strokeLineJoin=1) + p.addArc(cx1,cy1,radiusx,angle0,angle1,yradius=radiusy,moveTo=1) + p.lineTo(OX(i,angle1,0),OY(i,angle1,0)) + p.addArc(cx0,cy0,radiusx,angle0,angle1,yradius=radiusy,reverse=1) + p.closePath() + if angle0<=_3dva and angle1>=_3dva: + rd = 0 + else: + rd = min(rad_dist(angle0),rad_dist(angle1)) + S.append((rd,p)) + _fillSide(S,i,lo,strokeColor,strokeWidth,fillColor) + _fillSide(S,i,hi,strokeColor,strokeWidth,fillColor) + + #bright shaded top + fillColor = style.fillColor + strokeColor = style.strokeColor or fillColor + T.append(Wedge(cx0,cy0,radiusx,lo,hi,yradius=radiusy, + strokeColor=strokeColor,strokeWidth=strokeWidth,fillColor=fillColor,strokeLineJoin=1)) + + text = labels[i] + if text: + rat = style.labelRadius + self._radiusx *= rat + self._radiusy *= rat + mid = sl.mid + labelX = OX(i,mid,0) + labelY = OY(i,mid,0) + _addWedgeLabel(self,text,L.append,mid,labelX,labelY,style,labelClass=WedgeLabel3d) + if checkLabelOverlap: + l = L[-1] + l._origdata = { 'x': labelX, 'y':labelY, 'angle': mid, + 'rx': self._radiusx, 'ry':self._radiusy, 'cx':CX(i,0), 'cy':CY(i,0), + 'bounds': l.getBounds(), + } + self._radiusx = radiusx + self._radiusy = radiusy + + S.sort(lambda a,b: -cmp(a[0],b[0])) + if checkLabelOverlap: + fixLabelOverlaps(L) + map(g.add,map(lambda x:x[1],S)+T+L) + return g + + def demo(self): + d = Drawing(200, 100) + + pc = Pie() + pc.x = 50 + pc.y = 10 + pc.width = 100 + pc.height = 80 + pc.data = [10,20,30,40,50,60] + pc.labels = ['a','b','c','d','e','f'] + + pc.slices.strokeWidth=0.5 + pc.slices[3].popout = 10 + pc.slices[3].strokeWidth = 2 + pc.slices[3].strokeDashArray = [2,2] + pc.slices[3].labelRadius = 1.75 + pc.slices[3].fontColor = colors.red + pc.slices[0].fillColor = colors.darkcyan + pc.slices[1].fillColor = colors.blueviolet + pc.slices[2].fillColor = colors.blue + pc.slices[3].fillColor = colors.cyan + pc.slices[4].fillColor = colors.aquamarine + pc.slices[5].fillColor = colors.cadetblue + pc.slices[6].fillColor = colors.lightcoral + self.slices[1].visible = 0 + self.slices[3].visible = 1 + self.slices[4].visible = 1 + self.slices[5].visible = 1 + self.slices[6].visible = 0 + + d.add(pc) + return d + + +def sample0a(): + "Make a degenerated pie chart with only one slice." + + d = Drawing(400, 200) + + pc = Pie() + pc.x = 150 + pc.y = 50 + pc.data = [10] + pc.labels = ['a'] + pc.slices.strokeWidth=1#0.5 + + d.add(pc) + + return d + + +def sample0b(): + "Make a degenerated pie chart with only one slice." + + d = Drawing(400, 200) + + pc = Pie() + pc.x = 150 + pc.y = 50 + pc.width = 120 + pc.height = 100 + pc.data = [10] + pc.labels = ['a'] + pc.slices.strokeWidth=1#0.5 + + d.add(pc) + + return d + + +def sample1(): + "Make a typical pie chart with with one slice treated in a special way." + + d = Drawing(400, 200) + + pc = Pie() + pc.x = 150 + pc.y = 50 + pc.data = [10, 20, 30, 40, 50, 60] + pc.labels = ['a', 'b', 'c', 'd', 'e', 'f'] + + pc.slices.strokeWidth=1#0.5 + pc.slices[3].popout = 20 + pc.slices[3].strokeWidth = 2 + pc.slices[3].strokeDashArray = [2,2] + pc.slices[3].labelRadius = 1.75 + pc.slices[3].fontColor = colors.red + + d.add(pc) + + return d + + +def sample2(): + "Make a pie chart with nine slices." + + d = Drawing(400, 200) + + pc = Pie() + pc.x = 125 + pc.y = 25 + pc.data = [0.31, 0.148, 0.108, + 0.076, 0.033, 0.03, + 0.019, 0.126, 0.15] + pc.labels = ['1', '2', '3', '4', '5', '6', '7', '8', 'X'] + + pc.width = 150 + pc.height = 150 + pc.slices.strokeWidth=1#0.5 + + pc.slices[0].fillColor = colors.steelblue + pc.slices[1].fillColor = colors.thistle + pc.slices[2].fillColor = colors.cornflower + pc.slices[3].fillColor = colors.lightsteelblue + pc.slices[4].fillColor = colors.aquamarine + pc.slices[5].fillColor = colors.cadetblue + pc.slices[6].fillColor = colors.lightcoral + pc.slices[7].fillColor = colors.tan + pc.slices[8].fillColor = colors.darkseagreen + + d.add(pc) + + return d + + +def sample3(): + "Make a pie chart with a very slim slice." + + d = Drawing(400, 200) + + pc = Pie() + pc.x = 125 + pc.y = 25 + + pc.data = [74, 1, 25] + + pc.width = 150 + pc.height = 150 + pc.slices.strokeWidth=1#0.5 + pc.slices[0].fillColor = colors.steelblue + pc.slices[1].fillColor = colors.thistle + pc.slices[2].fillColor = colors.cornflower + + d.add(pc) + + return d + + +def sample4(): + "Make a pie chart with several very slim slices." + + d = Drawing(400, 200) + + pc = Pie() + pc.x = 125 + pc.y = 25 + + pc.data = [74, 1, 1, 1, 1, 22] + + pc.width = 150 + pc.height = 150 + pc.slices.strokeWidth=1#0.5 + pc.slices[0].fillColor = colors.steelblue + pc.slices[1].fillColor = colors.thistle + pc.slices[2].fillColor = colors.cornflower + pc.slices[3].fillColor = colors.lightsteelblue + pc.slices[4].fillColor = colors.aquamarine + pc.slices[5].fillColor = colors.cadetblue + + d.add(pc) + + return d diff --git a/bin/reportlab/graphics/charts/slidebox.py b/bin/reportlab/graphics/charts/slidebox.py new file mode 100644 index 00000000000..309fb07ba0d --- /dev/null +++ b/bin/reportlab/graphics/charts/slidebox.py @@ -0,0 +1,186 @@ +from reportlab.lib.colors import Color, white, black +from reportlab.graphics.charts.textlabels import Label +from reportlab.graphics.shapes import Polygon, Line, Circle, String, Drawing, PolyLine, Group, Rect +from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection +from reportlab.lib.attrmap import * +from reportlab.lib.validators import * +from reportlab.lib.units import cm +from reportlab.pdfbase.pdfmetrics import stringWidth, getFont +from reportlab.graphics.widgets.grids import ShadedRect, Grid + +class SlideBox(Widget): + """Returns a slidebox widget""" + _attrMap = AttrMap( + labelFontName = AttrMapValue(isString, desc="Name of font used for the labels"), + labelFontSize = AttrMapValue(isNumber, desc="Size of font used for the labels"), + labelStrokeColor = AttrMapValue(isColorOrNone, desc="Colour for for number outlines"), + labelFillColor = AttrMapValue(isColorOrNone, desc="Colour for number insides"), + startColor = AttrMapValue(isColor, desc='Color of first box'), + endColor = AttrMapValue(isColor, desc='Color of last box'), + numberOfBoxes = AttrMapValue(isInt, desc='How many boxes there are'), + trianglePosition = AttrMapValue(isInt, desc='Which box is highlighted by the triangles'), + triangleHeight = AttrMapValue(isNumber, desc="Height of indicator triangles"), + triangleWidth = AttrMapValue(isNumber, desc="Width of indicator triangles"), + triangleFillColor = AttrMapValue(isColor, desc="Colour of indicator triangles"), + triangleStrokeColor = AttrMapValue(isColorOrNone, desc="Colour of indicator triangle outline"), + triangleStrokeWidth = AttrMapValue(isNumber, desc="Colour of indicator triangle outline"), + boxHeight = AttrMapValue(isNumber, desc="Height of the boxes"), + boxWidth = AttrMapValue(isNumber, desc="Width of the boxes"), + boxSpacing = AttrMapValue(isNumber, desc="Space between the boxes"), + boxOutlineColor = AttrMapValue(isColorOrNone, desc="Colour used to outline the boxes (if any)"), + boxOutlineWidth = AttrMapValue(isNumberOrNone, desc="Width of the box outline (if any)"), + leftPadding = AttrMapValue(isNumber, desc='Padding on left of drawing'), + rightPadding = AttrMapValue(isNumber, desc='Padding on right of drawing'), + topPadding = AttrMapValue(isNumber, desc='Padding at top of drawing'), + bottomPadding = AttrMapValue(isNumber, desc='Padding at bottom of drawing'), + background = AttrMapValue(isColorOrNone, desc='Colour of the background to the drawing (if any)'), + sourceLabelText = AttrMapValue(isNoneOrString, desc="Text used for the 'source' label (can be empty)"), + sourceLabelOffset = AttrMapValue(isNumber, desc='Padding at bottom of drawing'), + sourceLabelFontName = AttrMapValue(isString, desc="Name of font used for the 'source' label"), + sourceLabelFontSize = AttrMapValue(isNumber, desc="Font size for the 'source' label"), + sourceLabelFillColor = AttrMapValue(isColorOrNone, desc="Colour ink for the 'source' label (bottom right)"), + ) + + def __init__(self): + self.labelFontName = "Helvetica-Bold" + self.labelFontSize = 10 + self.labelStrokeColor = black + self.labelFillColor = white + self.startColor = colors.Color(232/255.0,224/255.0,119/255.0) + self.endColor = colors.Color(25/255.0,77/255.0,135/255.0) + self.numberOfBoxes = 7 + self.trianglePosition = 7 + self.triangleHeight = 0.12*cm + self.triangleWidth = 0.38*cm + self.triangleFillColor = white + self.triangleStrokeColor = black + self.triangleStrokeWidth = 0.58 + self.boxHeight = 0.55*cm + self.boxWidth = 0.73*cm + self.boxSpacing = 0.075*cm + self.boxOutlineColor = black + self.boxOutlineWidth = 0.58 + self.leftPadding=5 + self.rightPadding=5 + self.topPadding=5 + self.bottomPadding=5 + self.background=None + self.sourceLabelText = "Source: ReportLab" + self.sourceLabelOffset = 0.2*cm + self.sourceLabelFontName = "Helvetica-Oblique" + self.sourceLabelFontSize = 6 + self.sourceLabelFillColor = black + + def _getDrawingDimensions(self): + tx=(self.numberOfBoxes*self.boxWidth) + if self.numberOfBoxes>1: tx=tx+((self.numberOfBoxes-1)*self.boxSpacing) + tx=tx+self.leftPadding+self.rightPadding + ty=self.boxHeight+self.triangleHeight + ty=ty+self.topPadding+self.bottomPadding+self.sourceLabelOffset+self.sourceLabelFontSize + return (tx,ty) + + def _getColors(self): + # for calculating intermediate colors... + numShades = self.numberOfBoxes+1 + fillColorStart = self.startColor + fillColorEnd = self.endColor + colorsList =[] + + for i in range(0,numShades): + colorsList.append(colors.linearlyInterpolatedColor(fillColorStart, fillColorEnd, 0, numShades-1, i)) + return colorsList + + def demo(self,drawing=None): + from reportlab.lib import colors + if not drawing: + tx,ty=self._getDrawingDimensions() + drawing = Drawing(tx,ty) + drawing.add(self.draw()) + return drawing + + def draw(self): + g = Group() + ys = self.bottomPadding+(self.triangleHeight/2)+self.sourceLabelOffset+self.sourceLabelFontSize + if self.background: + x,y = self._getDrawingDimensions() + g.add(Rect(-self.leftPadding,-ys,x,y, + strokeColor=None, + strokeWidth=0, + fillColor=self.background)) + + ascent=getFont(self.labelFontName).face.ascent/1000. + if ascent==0: ascent=0.718 # default (from helvetica) + ascent=ascent*self.labelFontSize # normalize + + colorsList = self._getColors() + + # Draw the boxes - now uses ShadedRect from grids + x=0 + for f in range (0,self.numberOfBoxes): + sr=ShadedRect() + sr.x=x + sr.y=0 + sr.width=self.boxWidth + sr.height=self.boxHeight + sr.orientation = 'vertical' + sr.numShades = 30 + sr.fillColorStart = colorsList[f] + sr.fillColorEnd = colorsList[f+1] + sr.strokeColor = None + sr.strokeWidth = 0 + + g.add(sr) + + g.add(Rect(x,0,self.boxWidth,self.boxHeight, + strokeColor=self.boxOutlineColor, + strokeWidth=self.boxOutlineWidth, + fillColor=None)) + + g.add(String(x+self.boxWidth/2.,(self.boxHeight-ascent)/2., + text = str(f+1), + fillColor = self.labelFillColor, + strokeColor=self.labelStrokeColor, + textAnchor = 'middle', + fontName = self.labelFontName, + fontSize = self.labelFontSize)) + x=x+self.boxWidth+self.boxSpacing + + #do triangles + xt = (self.trianglePosition*self.boxWidth) + if self.trianglePosition>1: + xt = xt+(self.trianglePosition-1)*self.boxSpacing + xt = xt-(self.boxWidth/2) + g.add(Polygon( + strokeColor = self.triangleStrokeColor, + strokeWidth = self.triangleStrokeWidth, + fillColor = self.triangleFillColor, + points=[xt,self.boxHeight-(self.triangleHeight/2), + xt-(self.triangleWidth/2),self.boxHeight+(self.triangleHeight/2), + xt+(self.triangleWidth/2),self.boxHeight+(self.triangleHeight/2), + xt,self.boxHeight-(self.triangleHeight/2)])) + g.add(Polygon( + strokeColor = self.triangleStrokeColor, + strokeWidth = self.triangleStrokeWidth, + fillColor = self.triangleFillColor, + points=[xt,0+(self.triangleHeight/2), + xt-(self.triangleWidth/2),0-(self.triangleHeight/2), + xt+(self.triangleWidth/2),0-(self.triangleHeight/2), + xt,0+(self.triangleHeight/2)])) + + #source label + if self.sourceLabelText != None: + g.add(String(x-self.boxSpacing,0-(self.triangleHeight/2)-self.sourceLabelOffset-(self.sourceLabelFontSize), + text = self.sourceLabelText, + fillColor = self.sourceLabelFillColor, + textAnchor = 'end', + fontName = self.sourceLabelFontName, + fontSize = self.sourceLabelFontSize)) + + g.shift(self.leftPadding, ys) + + return g + + +if __name__ == "__main__": + d = SlideBox() + d.demo().save(fnRoot="slidebox") diff --git a/bin/reportlab/graphics/charts/spider.py b/bin/reportlab/graphics/charts/spider.py new file mode 100644 index 00000000000..6de92724390 --- /dev/null +++ b/bin/reportlab/graphics/charts/spider.py @@ -0,0 +1,407 @@ + #Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/spider.py +# spider chart, also known as radar chart + +"""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. +""" +__version__=''' $Id: spider.py 2676 2005-09-06 10:25:00Z rgbecker $ ''' + +import copy +from math import sin, cos, pi + +from reportlab.lib import colors +from reportlab.lib.validators import isColor, isNumber, isListOfNumbersOrNone,\ + isListOfNumbers, isColorOrNone, isString,\ + isListOfStringsOrNone, OneOf, SequenceOf,\ + isBoolean, isListOfColors, isNumberOrNone,\ + isNoneOrListOfNoneOrStrings, isTextAnchor,\ + isNoneOrListOfNoneOrNumbers, isBoxAnchor,\ + isStringOrNone, isStringOrNone, EitherOr,\ + isCallable +from reportlab.lib.attrmap import * +from reportlab.pdfgen.canvas import Canvas +from reportlab.graphics.shapes import Group, Drawing, Line, Rect, Polygon, PolyLine, Ellipse, \ + Wedge, String, STATE_DEFAULTS +from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder +from reportlab.graphics.charts.areas import PlotArea +from piecharts import WedgeLabel +from reportlab.graphics.widgets.markers import makeMarker, uSymbol2Symbol, isSymbol + +class StrandProperty(PropHolder): + + _attrMap = AttrMap( + strokeWidth = AttrMapValue(isNumber), + fillColor = AttrMapValue(isColorOrNone), + strokeColor = AttrMapValue(isColorOrNone), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone), + symbol = AttrMapValue(EitherOr((isStringOrNone,isSymbol)), desc='Widget placed at data points.'), + symbolSize= AttrMapValue(isNumber, desc='Symbol size.'), + 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), + fillColor = AttrMapValue(isColorOrNone), + strokeColor = AttrMapValue(isColorOrNone), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone), + labelRadius = AttrMapValue(isNumber), + 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 kw.keys(): self._text = '' + +class StrandLabel(SpokeLabel): + _attrMap = AttrMap(BASE=SpokeLabel, + format = AttrMapValue(EitherOr((isStringOrNone,isCallable)),"Format for the label"), + dR = AttrMapValue(isNumberOrNone,"radial shift for label"), + ) + def __init__(self,**kw): + self.format = '' + self.dR = 0 + SpokeLabel.__init__(self,**kw) + +def _setupLabel(labelClass, text, radius, cx, cy, angle, car, sar, sty): + L = labelClass() + L._text = text + L.x = cx + radius*car + L.y = cy + radius*sar + L._pmv = angle*180/pi + L.boxAnchor = sty.boxAnchor + L.dx = sty.dx + L.dy = sty.dy + L.angle = sty.angle + L.boxAnchor = sty.boxAnchor + L.boxStrokeColor = sty.boxStrokeColor + L.boxStrokeWidth = sty.boxStrokeWidth + L.boxFillColor = sty.boxFillColor + L.strokeColor = sty.strokeColor + L.strokeWidth = sty.strokeWidth + L.leading = sty.leading + L.width = sty.width + L.maxWidth = sty.maxWidth + L.height = sty.height + L.textAnchor = sty.textAnchor + L.visible = sty.visible + L.topPadding = sty.topPadding + L.leftPadding = sty.leftPadding + L.rightPadding = sty.rightPadding + L.bottomPadding = sty.bottomPadding + L.fontName = sty.fontName + L.fontSize = sty.fontSize + L.fillColor = sty.fillColor + return L + +class SpiderChart(PlotArea): + _attrMap = AttrMap(BASE=PlotArea, + data = AttrMapValue(None, desc='Data to be plotted, list of (lists of) numbers.'), + labels = AttrMapValue(isListOfStringsOrNone, desc="optional list of labels to use for each data point"), + startAngle = AttrMapValue(isNumber, desc="angle of first slice; like the compass, 0 is due North"), + direction = AttrMapValue( OneOf('clockwise', 'anticlockwise'), desc="'clockwise' or 'anticlockwise'"), + strands = AttrMapValue(None, desc="collection of strand descriptor objects"), + spokes = AttrMapValue(None, desc="collection of spoke descriptor objects"), + strandLabels = AttrMapValue(None, desc="collection of strand label descriptor objects"), + spokeLabels = AttrMapValue(None, desc="collection of spoke label descriptor objects"), + ) + + def makeSwatchSample(self, rowNo, x, y, width, height): + baseStyle = self.strands + styleIdx = rowNo % len(baseStyle) + style = baseStyle[styleIdx] + strokeColor = getattr(style, 'strokeColor', getattr(baseStyle,'strokeColor',None)) + fillColor = getattr(style, 'fillColor', getattr(baseStyle,'fillColor',None)) + strokeDashArray = getattr(style, 'strokeDashArray', getattr(baseStyle,'strokeDashArray',None)) + strokeWidth = getattr(style, 'strokeWidth', getattr(baseStyle, 'strokeWidth',0)) + symbol = getattr(style, 'symbol', getattr(baseStyle, 'symbol',None)) + ym = y+height/2.0 + if fillColor is None and strokeColor is not None and strokeWidth>0: + bg = Line(x,ym,x+width,ym,strokeWidth=strokeWidth,strokeColor=strokeColor, + strokeDashArray=strokeDashArray) + elif fillColor is not None: + bg = Rect(x,y,width,height,strokeWidth=strokeWidth,strokeColor=strokeColor, + strokeDashArray=strokeDashArray,fillColor=fillColor) + else: + bg = None + if symbol: + symbol = uSymbol2Symbol(symbol,x+width/2.,ym,color) + if bg: + g = Group() + g.add(bg) + g.add(symbol) + return g + return symbol or bg + + def getSeriesName(self,i,default=None): + '''return series name i or default''' + return 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(map(min,data)) >=0, "Cannot do spider plots of negative numbers!" + norm = max(map(max,data)) + norm *= (1.0+outer) + if norm<1e-9: norm = 1.0 + self._norm = norm + return [[e/norm for e in row] for row in data] + + def _innerDrawLabel(self, sty, radius, cx, cy, angle, car, sar, labelClass=StrandLabel): + "Draw a label for a given item in the list." + fmt = sty.format + value = radius*self._norm + if not fmt: + text = None + elif isinstance(fmt,str): + if fmt == 'values': + text = sty._text + else: + text = fmt % value + elif callable(fmt): + text = fmt(value) + else: + raise ValueError("Unknown formatter type %s, expected string or function" % fmt) + + if text: + dR = sty.dR + if dR: + radius += dR/self._radius + L = _setupLabel(labelClass, text, radius, cx, cy, angle, car, sar, sty) + if dR<0: L._anti = 1 + else: + L = None + return L + + def draw(self): + # normalize slice data + g = self.makeBackground() or Group() + + xradius = self.width/2.0 + yradius = self.height/2.0 + self._radius = radius = min(xradius, yradius) + cx = self.x + xradius + cy = self.y + yradius + + data = self.normalizeData() + + self._seriesCount = len(data) + n = len(data[0]) + + #labels + if self.labels is None: + labels = [''] * n + else: + labels = self.labels + #there's no point in raising errors for less than enough errors if + #we silently create all for the extreme case of no labels. + i = n-len(labels) + if i>0: + labels = labels + ['']*i + + S = [] + STRANDS = [] + STRANDAREAS = [] + syms = [] + labs = [] + csa = [] + angle = self.startAngle*pi/180 + direction = self.direction == "clockwise" and -1 or 1 + angleBetween = direction*(2 * pi)/float(n) + spokes = self.spokes + spokeLabels = self.spokeLabels + for i in xrange(n): + car = cos(angle)*radius + sar = sin(angle)*radius + csa.append((car,sar,angle)) + si = self.spokes[i] + if si.visible: + spoke = Line(cx, cy, cx + car, cy + sar, strokeWidth = si.strokeWidth, strokeColor=si.strokeColor, strokeDashArray=si.strokeDashArray) + S.append(spoke) + sli = spokeLabels[i] + text = sli._text + if not text: text = labels[i] + if text: + S.append(_setupLabel(WedgeLabel, text, si.labelRadius, cx, cy, angle, car, sar, sli)) + angle += angleBetween + + # now plot the polygons + rowIdx = 0 + strands = self.strands + strandLabels = self.strandLabels + for row in data: + # series plot + rsty = strands[rowIdx] + points = [] + car, sar = csa[-1][:2] + r = row[-1] + points.append(cx+car*r) + points.append(cy+sar*r) + for i in xrange(n): + car, sar, angle = csa[i] + r = row[i] + points.append(cx+car*r) + points.append(cy+sar*r) + L = self._innerDrawLabel(strandLabels[(rowIdx,i)], r, cx, cy, angle, car, sar, labelClass=StrandLabel) + if L: labs.append(L) + sty = strands[(rowIdx,i)] + uSymbol = sty.symbol + + # put in a marker, if it needs one + if uSymbol: + s_x = cx+car*r + s_y = cy+sar*r + s_fillColor = sty.fillColor + s_strokeColor = sty.strokeColor + s_strokeWidth = sty.strokeWidth + s_angle = 0 + s_size = sty.symbolSize + if type(uSymbol) is type(''): + symbol = makeMarker(uSymbol, + size = s_size, + x = s_x, + y = s_y, + fillColor = s_fillColor, + strokeColor = s_strokeColor, + strokeWidth = s_strokeWidth, + angle = s_angle, + ) + else: + symbol = uSymbol2Symbol(uSymbol,s_x,s_y,s_fillColor) + for k,v in (('size', s_size), ('fillColor', s_fillColor), + ('x', s_x), ('y', s_y), + ('strokeColor',s_strokeColor), ('strokeWidth',s_strokeWidth), + ('angle',s_angle),): + if getattr(symbol,k,None) is None: + try: + setattr(symbol,k,v) + except: + pass + syms.append(symbol) + + # make up the 'strand' + if rsty.fillColor: + strand = Polygon(points) + strand.fillColor = rsty.fillColor + strand.strokeColor = None + strand.strokeWidth = 0 + STRANDAREAS.append(strand) + if rsty.strokeColor and rsty.strokeWidth: + strand = PolyLine(points) + strand.strokeColor = rsty.strokeColor + strand.strokeWidth = rsty.strokeWidth + strand.strokeDashArray = rsty.strokeDashArray + STRANDS.append(strand) + rowIdx += 1 + + map(g.add,STRANDAREAS+STRANDS+syms+S+labs) + 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') diff --git a/bin/reportlab/graphics/charts/textlabels.py b/bin/reportlab/graphics/charts/textlabels.py new file mode 100644 index 00000000000..5bcd0250e79 --- /dev/null +++ b/bin/reportlab/graphics/charts/textlabels.py @@ -0,0 +1,442 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/textlabels.py +__version__=''' $Id: textlabels.py 2647 2005-07-26 13:47:51Z rgbecker $ ''' +import string + +from reportlab.lib import colors +from reportlab.lib.validators import isNumber, isNumberOrNone, OneOf, isColorOrNone, isString, \ + isTextAnchor, isBoxAnchor, isBoolean, NoneOr, isInstanceOf, isNoneOrString +from reportlab.lib.attrmap import * +from reportlab.pdfbase.pdfmetrics import stringWidth +from reportlab.graphics.shapes import Drawing, Group, Circle, Rect, String, STATE_DEFAULTS +from reportlab.graphics.shapes import _PATH_OP_ARG_COUNT, _PATH_OP_NAMES, definePath +from reportlab.graphics.widgetbase import Widget, PropHolder + +_gs = None +_A2BA= { + 'x': {0:'n', 45:'ne', 90:'e', 135:'se', 180:'s', 225:'sw', 270:'w', 315: 'nw', -45: 'nw'}, + 'y': {0:'e', 45:'se', 90:'s', 135:'sw', 180:'w', 225:'nw', 270:'n', 315: 'ne', -45: 'ne'}, + } +def _simpleSplit(txt,mW,SW): + L = [] + ws = SW(' ') + O = [] + w = -ws + for t in string.split(txt): + lt = SW(t) + if w+ws+lt<=mW or O==[]: + O.append(t) + w = w + ws + lt + else: + L.append(string.join(O,' ')) + O = [t] + w = lt + if O!=[]: L.append(string.join(O,' ')) + return L + +def _pathNumTrunc(n): + if int(n)==n: return int(n) + return round(n,5) + +def _processGlyph(G, truncate=1, pathReverse=0): + O = [] + P = [] + R = [] + for g in G+(('end',),): + op = g[0] + if O and op in ['moveTo', 'moveToClosed','end']: + if O[0]=='moveToClosed': + O = O[1:] + if pathReverse: + for i in xrange(0,len(P),2): + P[i+1], P[i] = P[i:i+2] + P.reverse() + O.reverse() + O.insert(0,'moveTo') + O.append('closePath') + i = 0 + if truncate: P = map(_pathNumTrunc,P) + for o in O: + j = i + _PATH_OP_ARG_COUNT[_PATH_OP_NAMES.index(o)] + if o=='closePath': + R.append(o) + else: + R.append((o,)+ tuple(P[i:j])) + i = j + O = [] + P = [] + O.append(op) + P.extend(g[1:]) + return R + +def _text2PathDescription(text, x=0, y=0, fontName='Times-Roman', fontSize=1000, + anchor='start', truncate=1, pathReverse=0): + global _gs + if not _gs: + import _renderPM + _gs = _renderPM.gstate(1,1) + from reportlab.graphics import renderPM + renderPM._setFont(_gs,fontName,fontSize) + P = [] + if not anchor =='start': + textLen = stringWidth(text, fontName,fontSize) + if text_anchor=='end': + x = x-textLen + elif text_anchor=='middle': + x = x - textLen/2. + for g in _gs._stringPath(text,x,y): + P.extend(_processGlyph(g,truncate=truncate,pathReverse=pathReverse)) + return P + +def _text2Path(text, x=0, y=0, fontName='Times-Roman', fontSize=1000, + anchor='start', truncate=1, pathReverse=0): + return definePath(_text2PathDescription(text,x=x,y=y,fontName=fontName, + fontSize=fontSize,anchor=anchor,truncate=truncate,pathReverse=pathReverse)) + +_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), + y = AttrMapValue(isNumber), + dx = AttrMapValue(isNumber), + dy = AttrMapValue(isNumber), + angle = AttrMapValue(isNumber), + boxAnchor = AttrMapValue(isBoxAnchor), + boxStrokeColor = AttrMapValue(isColorOrNone), + boxStrokeWidth = AttrMapValue(isNumber), + boxFillColor = AttrMapValue(isColorOrNone), + boxTarget = AttrMapValue(isString), + fillColor = AttrMapValue(isColorOrNone), + strokeColor = AttrMapValue(isColorOrNone), + strokeWidth = AttrMapValue(isNumber), + text = AttrMapValue(isString), + fontName = AttrMapValue(isString), + fontSize = AttrMapValue(isNumber), + leading = AttrMapValue(isNumberOrNone), + width = AttrMapValue(isNumberOrNone), + maxWidth = AttrMapValue(isNumberOrNone), + height = AttrMapValue(isNumberOrNone), + textAnchor = AttrMapValue(isTextAnchor), + visible = AttrMapValue(isBoolean,desc="True if the label is to be drawn"), + topPadding = AttrMapValue(isNumber,'padding at top of box'), + leftPadding = AttrMapValue(isNumber,'padding at left of box'), + rightPadding = AttrMapValue(isNumber,'padding at right of box'), + bottomPadding = AttrMapValue(isNumber,'padding at bottom of box'), + ) + + def __init__(self,**kw): + self._setKeywords(**kw) + self._setKeywords( + _text = 'Multi-Line\nString', + boxAnchor = 'c', + angle = 0, + x = 0, + y = 0, + dx = 0, + dy = 0, + topPadding = 0, + leftPadding = 0, + rightPadding = 0, + bottomPadding = 0, + boxStrokeWidth = 0.5, + boxStrokeColor = None, + boxTarget = 'normal', + strokeColor = None, + boxFillColor = None, + leading = None, + width = None, + maxWidth = None, + height = None, + fillColor = STATE_DEFAULTS['fillColor'], + fontName = STATE_DEFAULTS['fontName'], + fontSize = STATE_DEFAULTS['fontSize'], + strokeWidth = 0.1, + textAnchor = 'start', + visible = 1, + ) + + def setText(self, text): + """Set the text property. May contain embedded newline characters. + Called by the containing chart or axis.""" + self._text = text + + + def setOrigin(self, x, y): + """Set the origin. This would be the tick mark or bar top relative to + which it is defined. Called by the containing chart or axis.""" + self.x = x + self.y = y + + + def demo(self): + """This shows a label positioned with its top right corner + at the top centre of the drawing, and rotated 45 degrees.""" + + d = Drawing(200, 100) + + # mark the origin of the label + d.add(Circle(100,90, 5, fillColor=colors.green)) + + lab = Label() + lab.setOrigin(100,90) + lab.boxAnchor = 'ne' + lab.angle = 45 + lab.dx = 0 + lab.dy = -20 + lab.boxStrokeColor = colors.green + lab.setText('Another\nMulti-Line\nString') + d.add(lab) + + return d + + def _getBoxAnchor(self): + '''hook for allowing special box anchor effects''' + ba = self.boxAnchor + if ba in ('autox', 'autoy'): + angle = self.angle + na = (int((angle%360)/45.)*45)%360 + if not (na % 90): # we have a right angle case + da = (angle - na) % 360 + if abs(da)>5: + na = na + (da>0 and 45 or -45) + ba = _A2BA[ba[-1]][na] + return ba + + def computeSize(self): + # the thing will draw in its own coordinate system + self._lines = string.split(self._text, '\n') + self._lineWidths = [] + topPadding = self.topPadding + leftPadding = self.leftPadding + rightPadding = self.rightPadding + bottomPadding = self.bottomPadding + SW = lambda text, fN=self.fontName, fS=self.fontSize: stringWidth(text, fN, fS) + if self.maxWidth: + L = [] + for l in self._lines: + L[-1:-1] = _simpleSplit(l,self.maxWidth,SW) + self._lines = L + if not self.width: + w = 0 + for line in self._lines: + thisWidth = SW(line) + self._lineWidths.append(thisWidth) + w = max(w,thisWidth) + self._width = w+leftPadding+rightPadding + else: + self._width = self.width + self._height = self.height or ((self.leading or 1.2*self.fontSize) * len(self._lines)+topPadding+bottomPadding) + self._ewidth = (self._width-leftPadding-rightPadding) + self._eheight = (self._height-topPadding-bottomPadding) + boxAnchor = self._getBoxAnchor() + if boxAnchor in ['n','ne','nw']: + self._top = -topPadding + elif boxAnchor in ['s','sw','se']: + self._top = self._height-topPadding + else: + self._top = 0.5*self._eheight + self._bottom = self._top - self._eheight + + if boxAnchor in ['ne','e','se']: + self._left = leftPadding - self._width + elif boxAnchor in ['nw','w','sw']: + self._left = leftPadding + else: + self._left = -self._ewidth*0.5 + self._right = self._left+self._ewidth + + def _getTextAnchor(self): + '''This can be overridden to allow special effects''' + ta = self.textAnchor + if ta=='boxauto': ta = _BA2TA[self._getBoxAnchor()] + return ta + + def draw(self): + _text = self._text + self._text = _text or '' + self.computeSize() + self._text = _text + g = Group() + g.translate(self.x + self.dx, self.y + self.dy) + g.rotate(self.angle) + + y = self._top - self.fontSize + textAnchor = self._getTextAnchor() + if textAnchor == 'start': + x = self._left + elif textAnchor == 'middle': + x = self._left + self._ewidth*0.5 + else: + x = self._right + + # paint box behind text just in case they + # fill it + if self.boxFillColor or (self.boxStrokeColor and self.boxStrokeWidth): + g.add(Rect( self._left-self.leftPadding, + self._bottom-self.bottomPadding, + self._width, + self._height, + strokeColor=self.boxStrokeColor, + strokeWidth=self.boxStrokeWidth, + fillColor=self.boxFillColor) + ) + + fillColor, fontName, fontSize = self.fillColor, self.fontName, self.fontSize + strokeColor, strokeWidth, leading = self.strokeColor, self.strokeWidth, (self.leading or 1.2*fontSize) + 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 = y - leading + else: + for line in self._lines: + s = String(x, y, line) + s.textAnchor = textAnchor + s.fontName = fontName + s.fontSize = fontSize + s.fillColor = fillColor + g.add(s) + y = y - leading + + return g + +class LabelDecorator: + _attrMap = AttrMap( + x = AttrMapValue(isNumberOrNone), + y = AttrMapValue(isNumberOrNone), + dx = AttrMapValue(isNumberOrNone), + dy = AttrMapValue(isNumberOrNone), + angle = AttrMapValue(isNumberOrNone), + boxAnchor = AttrMapValue(isBoxAnchor), + boxStrokeColor = AttrMapValue(isColorOrNone), + boxStrokeWidth = AttrMapValue(isNumberOrNone), + boxFillColor = AttrMapValue(isColorOrNone), + fillColor = AttrMapValue(isColorOrNone), + strokeColor = AttrMapValue(isColorOrNone), + strokeWidth = AttrMapValue(isNumberOrNone), + fontName = AttrMapValue(isNoneOrString), + fontSize = AttrMapValue(isNumberOrNone), + leading = AttrMapValue(isNumberOrNone), + width = AttrMapValue(isNumberOrNone), + maxWidth = AttrMapValue(isNumberOrNone), + height = AttrMapValue(isNumberOrNone), + textAnchor = AttrMapValue(isTextAnchor), + visible = AttrMapValue(isBoolean,desc="True if the label is to be drawn"), + ) + + def __init__(self): + self.textAnchor = 'start' + self.boxAnchor = 'w' + for a in self._attrMap.keys(): + if not hasattr(self,a): setattr(self,a,None) + + def decorate(self,l,L): + chart,g,rowNo,colNo,x,y,width,height,x00,y00,x0,y0 = l._callOutInfo + L.setText(chart.categoryAxis.categoryNames[colNo]) + g.add(L) + + def __call__(self,l): + from copy import deepcopy + L = Label() + for a,v in self.__dict__.items(): + if v is None: v = getattr(l,a,None) + setattr(L,a,v) + self.decorate(l,L) + +isOffsetMode=OneOf('high','low','bar','axis') +class LabelOffset(PropHolder): + _attrMap = AttrMap( + posMode = AttrMapValue(isOffsetMode,desc="Where to base +ve offset"), + pos = AttrMapValue(isNumber,desc='Value for positive elements'), + negMode = AttrMapValue(isOffsetMode,desc="Where to base -ve offset"), + neg = AttrMapValue(isNumber,desc='Value for negative elements'), + ) + def __init__(self): + self.posMode=self.negMode='axis' + self.pos = self.neg = 0 + + def _getValue(self, chart, val): + flipXY = chart._flipXY + A = chart.categoryAxis + jA = A.joinAxis + if val>=0: + mode = self.posMode + delta = self.pos + else: + mode = self.negMode + delta = self.neg + if flipXY: + v = A._x + else: + v = A._y + if jA: + if flipXY: + _v = jA._x + else: + _v = jA._y + if mode=='high': + v = _v + jA._length + elif mode=='low': + v = _v + elif mode=='bar': + v = _v+val + return v+delta + +NoneOrInstanceOfLabelOffset=NoneOr(isInstanceOf(LabelOffset)) + +class BarChartLabel(Label): + """ + An extended Label allowing for nudging, lines visibility etc + """ + _attrMap = AttrMap( + BASE=Label, + 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"), + ) + + def __init__(self): + Label.__init__(self) + self.lineStrokeWidth = 0 + self.lineStrokeColor = None + self.nudge = 0 + self.fixedStart = self.fixedEnd = None + self._pmv = 0 + + def _getBoxAnchor(self): + a = self.boxAnchor + 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 = self.textAnchor + if self._pmv<0: a = {'start':'end', 'middle':'middle', 'end':'start'}[a] + return a + +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)) diff --git a/bin/reportlab/graphics/charts/utils.py b/bin/reportlab/graphics/charts/utils.py new file mode 100644 index 00000000000..c654428f5c1 --- /dev/null +++ b/bin/reportlab/graphics/charts/utils.py @@ -0,0 +1,191 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/utils.py +"Utilities used here and there." +__version__=''' $Id: utils.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' + +from time import mktime, gmtime, strftime +import string + + +### Dinu's stuff used in some line plots (likely to vansih). + +def mkTimeTuple(timeString): + "Convert a 'dd/mm/yyyy' formatted string to a tuple for use in the time module." + + list = [0] * 9 + dd, mm, yyyy = map(int, string.split(timeString, '/')) + list[:3] = [yyyy, mm, dd] + + return tuple(list) + + +def str2seconds(timeString): + "Convert a number of seconds since the epoch into a date string." + + return mktime(mkTimeTuple(timeString)) + + +def seconds2str(seconds): + "Convert a date string into the number of seconds since the epoch." + + return strftime('%Y-%m-%d', gmtime(seconds)) + + +### Aaron's rounding function for making nice values on axes. + +from math import log10 + +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 + + +### Robin's stuff from rgb_ticks. + +from math import log10, floor + +_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 = xrange(max(1,n-2),max(n+3,2)) + + w = 1e308 + for i in n: + z=find_interval(lower,upper,i) + if z[3] 3 or power < -3: + format = '%+'+`w+7`+'.0e' + else: + if power >= 0: + digits = int(power)+w + format = '%' + `digits`+'.0f' + else: + digits = w-int(power) + format = '%'+`digits+2`+'.'+`digits`+'f' + + if percent: format=format+'%%' + T = [] + n = int(float(hi-t)/grid+0.1)+1 + if split: + labels = [] + for i in xrange(n): + v = t+grid*i + T.append(v) + labels.append(format % v) + return T, labels + else: + for i in xrange(n): + v = t+grid*i + T.append((v, format % v)) + return T \ No newline at end of file diff --git a/bin/reportlab/graphics/charts/utils3d.py b/bin/reportlab/graphics/charts/utils3d.py new file mode 100644 index 00000000000..ca6d0f8d3d5 --- /dev/null +++ b/bin/reportlab/graphics/charts/utils3d.py @@ -0,0 +1,233 @@ +from reportlab.lib import colors +from reportlab.lib.attrmap import * +from reportlab.pdfgen.canvas import Canvas +from reportlab.graphics.shapes import Group, Drawing, Ellipse, Wedge, String, STATE_DEFAULTS, Polygon, Line + +def _getShaded(col,shd=None,shading=0.1): + if shd is None: + from reportlab.lib.colors import Blacker + if col: shd = Blacker(col,1-shading) + return shd + +def _getLit(col,shd=None,lighting=0.1): + if shd is None: + from reportlab.lib.colors import Whiter + if col: shd = Whiter(col,1-lighting) + return shd + + +def _draw_3d_bar(G, x1, x2, y0, yhigh, xdepth, ydepth, + fillColor=None, fillColorShaded=None, + strokeColor=None, strokeWidth=1, shading=0.1): + fillColorShaded = _getShaded(fillColor,None,shading) + fillColorShadedTop = _getShaded(fillColor,None,shading/2.0) + + def _add_3d_bar(x1, x2, y1, y2, xoff, yoff, + G=G,strokeColor=strokeColor, strokeWidth=strokeWidth, fillColor=fillColor): + G.add(Polygon((x1,y1, x1+xoff,y1+yoff, x2+xoff,y2+yoff, x2,y2), + strokeWidth=strokeWidth, strokeColor=strokeColor, fillColor=fillColor,strokeLineJoin=1)) + + usd = max(y0, yhigh) + if xdepth or ydepth: + if y0!=yhigh: #non-zero height + _add_3d_bar( x2, x2, y0, yhigh, xdepth, ydepth, fillColor=fillColorShaded) #side + + _add_3d_bar(x1, x2, usd, usd, xdepth, ydepth, fillColor=fillColorShadedTop) #top + + G.add(Polygon((x1,y0,x2,y0,x2,yhigh,x1,yhigh), + strokeColor=strokeColor, strokeWidth=strokeWidth, fillColor=fillColor,strokeLineJoin=1)) #front + + if xdepth or ydepth: + G.add(Line( x1, usd, x2, usd, strokeWidth=strokeWidth, strokeColor=strokeColor or fillColorShaded)) + +class _YStrip: + def __init__(self,y0,y1, slope, fillColor, fillColorShaded, shading=0.1): + self.y0 = y0 + self.y1 = y1 + self.slope = slope + self.fillColor = fillColor + self.fillColorShaded = _getShaded(fillColor,fillColorShaded,shading) + +def _ystrip_poly( x0, x1, y0, y1, xoff, yoff): + return [x0,y0,x0+xoff,y0+yoff,x1+xoff,y1+yoff,x1,y1] + + +def _make_3d_line_info( G, x0, x1, y0, y1, z0, z1, + theta_x, theta_y, + fillColor, fillColorShaded=None, tileWidth=1, + strokeColor=None, strokeWidth=None, strokeDashArray=None, + shading=0.1): + zwidth = abs(z1-z0) + xdepth = zwidth*theta_x + ydepth = zwidth*theta_y + depth_slope = xdepth==0 and 1e150 or -ydepth/float(xdepth) + + x = float(x1-x0) + slope = x==0 and 1e150 or (y1-y0)/x + + c = slope>depth_slope and _getShaded(fillColor,fillColorShaded,shading) or fillColor + zy0 = z0*theta_y + zx0 = z0*theta_x + + tileStrokeWidth = 0.6 + if tileWidth is None: + D = [(x1,y1)] + else: + T = ((y1-y0)**2+(x1-x0)**2)**0.5 + tileStrokeWidth *= tileWidth + if Tself.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-81 or ou<0 or ou>1: return + x = x0 + u*a + y = self.y0 + u*b + if _ZERO=small: a(seg) + S.sort(_segCmp) + I = [] + n = len(S) + for i in xrange(0,n-1): + s = S[i] + for j in xrange(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)]]) diff --git a/bin/reportlab/graphics/renderPDF.py b/bin/reportlab/graphics/renderPDF.py new file mode 100644 index 00000000000..e1da60c0896 --- /dev/null +++ b/bin/reportlab/graphics/renderPDF.py @@ -0,0 +1,360 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/renderPDF.py +# renderPDF - draws Drawings onto a canvas +"""Usage: + import renderpdf + renderpdf.draw(drawing, canvas, x, y) +Execute the script to see some test drawings. +changed +""" +__version__=''' $Id: renderPDF.py 2830 2006-04-05 15:18:32Z rgbecker $ ''' + +from reportlab.graphics.shapes import * +from reportlab.pdfgen.canvas import Canvas +from reportlab.pdfbase.pdfmetrics import stringWidth +from reportlab.lib.utils import getStringIO +from reportlab import rl_config +from renderbase import Renderer, StateTracker, getStateDelta, renderScaledDrawing + +# the main entry point for users... +def draw(drawing, canvas, x, y, showBoundary=rl_config._unset_): + """As it says""" + R = _PDFRenderer() + R.draw(renderScaledDrawing(drawing), canvas, x, y, showBoundary=showBoundary) + +class _PDFRenderer(Renderer): + """This draws onto a PDF document. It needs to be a class + rather than a function, as some PDF-specific state tracking is + needed outside of the state info in the SVG model.""" + + def __init__(self): + self._stroke = 0 + self._fill = 0 + self._tracker = StateTracker() + + def drawNode(self, node): + """This is the recursive method called for each node + in the tree""" + #print "pdf:drawNode", self + #if node.__class__ is Wedge: stop + if not (isinstance(node, Path) and node.isClipPath): + self._canvas.saveState() + + #apply state changes + deltas = getStateDelta(node) + self._tracker.push(deltas) + self.applyStateChanges(deltas, {}) + + #draw the object, or recurse + self.drawNodeDispatcher(node) + + self._tracker.pop() + if not (isinstance(node, Path) and node.isClipPath): + self._canvas.restoreState() + + def drawRect(self, rect): + if rect.rx == rect.ry == 0: + #plain old rectangle + self._canvas.rect( + rect.x, rect.y, + rect.width, rect.height, + stroke=self._stroke, + fill=self._fill + ) + else: + #cheat and assume ry = rx; better to generalize + #pdfgen roundRect function. TODO + self._canvas.roundRect( + rect.x, rect.y, + rect.width, rect.height, rect.rx, + fill=self._fill, + stroke=self._stroke + ) + + def drawImage(self, image): + # currently not implemented in other renderers + if image.path and os.path.exists(image.path): + self._canvas.drawInlineImage( + image.path, + image.x, image.y, + image.width, image.height + ) + + def drawLine(self, line): + if self._stroke: + self._canvas.line(line.x1, line.y1, line.x2, line.y2) + + def drawCircle(self, circle): + self._canvas.circle( + circle.cx, circle.cy, circle.r, + fill=self._fill, + stroke=self._stroke + ) + + def drawPolyLine(self, polyline): + if self._stroke: + assert len(polyline.points) >= 2, 'Polyline must have 2 or more points' + head, tail = polyline.points[0:2], polyline.points[2:], + path = self._canvas.beginPath() + path.moveTo(head[0], head[1]) + for i in range(0, len(tail), 2): + path.lineTo(tail[i], tail[i+1]) + self._canvas.drawPath(path) + + def drawWedge(self, wedge): + centerx, centery, radius, startangledegrees, endangledegrees = \ + wedge.centerx, wedge.centery, wedge.radius, wedge.startangledegrees, wedge.endangledegrees + yradius, radius1, yradius1 = wedge._xtraRadii() + if yradius is None: yradius = radius + angle = endangledegrees-startangledegrees + path = self._canvas.beginPath() + if (radius1==0 or radius1 is None) and (yradius1==0 or yradius1 is None): + path.moveTo(centerx, centery) + path.arcTo(centerx-radius, centery-yradius, centerx+radius, centery+yradius, + startangledegrees, angle) + else: + path.arc(centerx-radius, centery-yradius, centerx+radius, centery+yradius, + startangledegrees, angle) + path.arcTo(centerx-radius1, centery-yradius1, centerx+radius1, centery+yradius1, + endangledegrees, -angle) + path.close() + self._canvas.drawPath(path, + fill=self._fill, + stroke=self._stroke) + + def drawEllipse(self, ellipse): + #need to convert to pdfgen's bounding box representation + x1 = ellipse.cx - ellipse.rx + x2 = ellipse.cx + ellipse.rx + y1 = ellipse.cy - ellipse.ry + y2 = ellipse.cy + ellipse.ry + self._canvas.ellipse(x1,y1,x2,y2,fill=self._fill,stroke=self._stroke) + + def drawPolygon(self, polygon): + assert len(polygon.points) >= 2, 'Polyline must have 2 or more points' + head, tail = polygon.points[0:2], polygon.points[2:], + path = self._canvas.beginPath() + path.moveTo(head[0], head[1]) + for i in range(0, len(tail), 2): + path.lineTo(tail[i], tail[i+1]) + path.close() + self._canvas.drawPath( + path, + stroke=self._stroke, + fill=self._fill + ) + + def drawString(self, stringObj): + if self._fill: + S = self._tracker.getState() + text_anchor, x, y, text, enc = S['textAnchor'], stringObj.x,stringObj.y,stringObj.text, stringObj.encoding + if not text_anchor in ['start','inherited']: + font, font_size = S['fontName'], S['fontSize'] + textLen = stringWidth(text, font, font_size, enc) + if text_anchor=='end': + x = x-textLen + elif text_anchor=='middle': + x = x - textLen/2 + else: + raise ValueError, 'bad value for textAnchor '+str(text_anchor) + t = self._canvas.beginText(x,y) + t.textLine(text) + self._canvas.drawText(t) + + def drawPath(self, path): + from reportlab.graphics.shapes import _renderPath + pdfPath = self._canvas.beginPath() + drawFuncs = (pdfPath.moveTo, pdfPath.lineTo, pdfPath.curveTo, pdfPath.close) + isClosed = _renderPath(path, drawFuncs) + if isClosed: + fill = self._fill + else: + fill = 0 + if path.isClipPath: + self._canvas.clipPath(pdfPath, fill=fill, stroke=self._stroke) + else: + self._canvas.drawPath(pdfPath, + fill=fill, + stroke=self._stroke) + + def applyStateChanges(self, delta, newState): + """This takes a set of states, and outputs the PDF operators + needed to set those properties""" + for key, value in delta.items(): + if key == 'transform': + self._canvas.transform(value[0], value[1], value[2], + value[3], value[4], value[5]) + elif key == 'strokeColor': + #this has different semantics in PDF to SVG; + #we always have a color, and either do or do + #not apply it; in SVG one can have a 'None' color + if value is None: + self._stroke = 0 + else: + self._stroke = 1 + self._canvas.setStrokeColor(value) + elif key == 'strokeWidth': + self._canvas.setLineWidth(value) + elif key == 'strokeLineCap': #0,1,2 + self._canvas.setLineCap(value) + elif key == 'strokeLineJoin': + self._canvas.setLineJoin(value) +# elif key == 'stroke_dasharray': +# self._canvas.setDash(array=value) + elif key == 'strokeDashArray': + if value: + self._canvas.setDash(value) + else: + self._canvas.setDash() + elif key == 'fillColor': + #this has different semantics in PDF to SVG; + #we always have a color, and either do or do + #not apply it; in SVG one can have a 'None' color + if value is None: + self._fill = 0 + else: + self._fill = 1 + self._canvas.setFillColor(value) + elif key in ['fontSize', 'fontName']: + # both need setting together in PDF + # one or both might be in the deltas, + # so need to get whichever is missing + fontname = delta.get('fontName', self._canvas._fontname) + fontsize = delta.get('fontSize', self._canvas._fontsize) + self._canvas.setFont(fontname, fontsize) + +from reportlab.platypus import Flowable +class GraphicsFlowable(Flowable): + """Flowable wrapper around a Pingo drawing""" + def __init__(self, drawing): + self.drawing = drawing + self.width = self.drawing.width + self.height = self.drawing.height + + def draw(self): + draw(self.drawing, self.canv, 0, 0) + +def drawToFile(d, fn, msg="", showBoundary=rl_config._unset_, autoSize=1): + """Makes a one-page PDF with just the drawing. + + If autoSize=1, the PDF will be the same size as + the drawing; if 0, it will place the drawing on + an A4 page with a title above it - possibly overflowing + if too big.""" + d = renderScaledDrawing(d) + c = Canvas(fn) + c.setFont('Times-Roman', 36) + c.drawString(80, 750, msg) + c.setTitle(msg) + + if autoSize: + c.setPageSize((d.width, d.height)) + draw(d, c, 0, 0, showBoundary=showBoundary) + else: + #show with a title + c.setFont('Times-Roman', 12) + y = 740 + i = 1 + y = y - d.height + draw(d, c, 80, y, showBoundary=showBoundary) + + c.showPage() + c.save() + if sys.platform=='mac' and not hasattr(fn, "write"): + try: + import macfs, macostools + macfs.FSSpec(fn).SetCreatorType("CARO", "PDF ") + macostools.touched(fn) + except: + pass + + +def drawToString(d, msg="", showBoundary=rl_config._unset_,autoSize=1): + "Returns a PDF as a string in memory, without touching the disk" + s = getStringIO() + drawToFile(d, s, msg=msg, showBoundary=showBoundary,autoSize=autoSize) + return s.getvalue() + + +######################################################### +# +# test code. First, defin a bunch of drawings. +# Routine to draw them comes at the end. +# +######################################################### + + +def test(): + c = Canvas('renderPDF.pdf') + c.setFont('Times-Roman', 36) + c.drawString(80, 750, 'Graphics Test') + + # print all drawings and their doc strings from the test + # file + + #grab all drawings from the test module + from reportlab.graphics import testshapes + drawings = [] + for funcname in dir(testshapes): + if funcname[0:10] == 'getDrawing': + drawing = eval('testshapes.' + funcname + '()') #execute it + docstring = eval('testshapes.' + funcname + '.__doc__') + drawings.append((drawing, docstring)) + + #print in a loop, with their doc strings + c.setFont('Times-Roman', 12) + y = 740 + i = 1 + for (drawing, docstring) in drawings: + assert (docstring is not None), "Drawing %d has no docstring!" % i + if y < 300: #allows 5-6 lines of text + c.showPage() + y = 740 + # draw a title + y = y - 30 + c.setFont('Times-BoldItalic',12) + c.drawString(80, y, 'Drawing %d' % i) + c.setFont('Times-Roman',12) + y = y - 14 + textObj = c.beginText(80, y) + textObj.textLines(docstring) + c.drawText(textObj) + y = textObj.getY() + y = y - drawing.height + draw(drawing, c, 80, y) + i = i + 1 + if y!=740: c.showPage() + + c.save() + print 'saved renderPDF.pdf' + +##def testFlowable(): +## """Makes a platypus document""" +## from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer +## from reportlab.lib.styles import getSampleStyleSheet +## styles = getSampleStyleSheet() +## styNormal = styles['Normal'] +## +## doc = SimpleDocTemplate('test_flowable.pdf') +## story = [] +## story.append(Paragraph("This sees is a drawing can work as a flowable", styNormal)) +## +## import testdrawings +## drawings = [] +## +## for funcname in dir(testdrawings): +## if funcname[0:10] == 'getDrawing': +## drawing = eval('testdrawings.' + funcname + '()') #execute it +## docstring = eval('testdrawings.' + funcname + '.__doc__') +## story.append(Paragraph(docstring, styNormal)) +## story.append(Spacer(18,18)) +## story.append(drawing) +## story.append(Spacer(36,36)) +## +## doc.build(story) +## print 'saves test_flowable.pdf' + +if __name__=='__main__': + test() + #testFlowable() diff --git a/bin/reportlab/graphics/renderPM.py b/bin/reportlab/graphics/renderPM.py new file mode 100644 index 00000000000..35960a25b75 --- /dev/null +++ b/bin/reportlab/graphics/renderPM.py @@ -0,0 +1,663 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history www.reportlab.co.uk/rl-cgi/viewcvs.cgi/rlextra/graphics/Csrc/renderPM/renderP.py +__version__=''' $Id: renderPM.py 2830 2006-04-05 15:18:32Z rgbecker $ ''' +"""Usage: + from reportlab.graphics import renderPM + renderPM.drawToFile(drawing,filename,fmt='GIF',configPIL={....}) +Other functions let you create a PM drawing as string or into a PM buffer. +Execute the script to see some test drawings.""" + +from reportlab.graphics.shapes import * +from reportlab.graphics.renderbase import StateTracker, getStateDelta, renderScaledDrawing +from reportlab.pdfbase.pdfmetrics import getFont, unicode2T1 +from math import sin, cos, pi, ceil +from reportlab.lib.utils import getStringIO, open_and_read +from reportlab import rl_config + +class RenderPMError(Exception): + pass + +import string, os, sys + +try: + import _renderPM +except ImportError, errMsg: + raise ImportError, "No module named _renderPM\n" + \ + (str(errMsg)!='No module named _renderPM' and "it may be the wrong version or badly installed!" or + "see http://www.reportlab.org/rl_addons.html") + +from types import TupleType, ListType +_SeqTypes = (TupleType,ListType) + +def _getImage(): + try: + from PIL import Image + except ImportError: + import Image + return Image + +def Color2Hex(c): + #assert isinstance(colorobj, colors.Color) #these checks don't work well RGB + if c: return ((0xFF&int(255*c.red)) << 16) | ((0xFF&int(255*c.green)) << 8) | (0xFF&int(255*c.blue)) + return c + +# the main entry point for users... +def draw(drawing, canvas, x, y, showBoundary=rl_config._unset_): + """As it says""" + R = _PMRenderer() + R.draw(renderScaledDrawing(drawing), canvas, x, y, showBoundary=showBoundary) + +from reportlab.graphics.renderbase import Renderer +class _PMRenderer(Renderer): + """This draws onto a pix map image. It needs to be a class + rather than a function, as some image-specific state tracking is + needed outside of the state info in the SVG model.""" + + def __init__(self): + self._tracker = StateTracker() + + def pop(self): + self._tracker.pop() + self.applyState() + + def push(self,node): + deltas = getStateDelta(node) + self._tracker.push(deltas) + self.applyState() + + def applyState(self): + s = self._tracker.getState() + self._canvas.ctm = s['ctm'] + self._canvas.strokeWidth = s['strokeWidth'] + self._canvas.strokeColor = Color2Hex(s['strokeColor']) + self._canvas.lineCap = s['strokeLineCap'] + self._canvas.lineJoin = s['strokeLineJoin'] + da = s['strokeDashArray'] + da = da and (0,da) or None + self._canvas.dashArray = da + self._canvas.fillColor = Color2Hex(s['fillColor']) + self._canvas.setFont(s['fontName'], s['fontSize']) + + def initState(self,x,y): + deltas = STATE_DEFAULTS.copy() + deltas['transform'] = self._canvas._baseCTM[0:4]+(x,y) + self._tracker.push(deltas) + self.applyState() + + def drawNode(self, node): + """This is the recursive method called for each node + in the tree""" + + #apply state changes + self.push(node) + + #draw the object, or recurse + self.drawNodeDispatcher(node) + + # restore the state + self.pop() + + def drawRect(self, rect): + c = self._canvas + if rect.rx == rect.ry == 0: + #plain old rectangle, draw clockwise (x-axis to y-axis) direction + c.rect(rect.x,rect.y, rect.width, rect.height) + else: + c.roundRect(rect.x,rect.y, rect.width, rect.height, rect.rx, rect.ry) + + def drawLine(self, line): + self._canvas.line(line.x1,line.y1,line.x2,line.y2) + + def drawImage(self, image): + if image.path and os.path.exists(image.path): + if type(image.path) is type(''): + im = _getImage().open(image.path).convert('RGB') + else: + im = image.path.convert('RGB') + srcW, srcH = im.size + dstW, dstH = image.width, image.height + if dstW is None: dstW = srcW + if dstH is None: dstH = srcH + self._canvas._aapixbuf( + image.x, image.y, dstW, dstH, + im.tostring(), srcW, srcH, 3, + ) + + def drawCircle(self, circle): + c = self._canvas + c.circle(circle.cx,circle.cy, circle.r) + c.fillstrokepath() + + def drawPolyLine(self, polyline, _doClose=0): + P = polyline.points + assert len(P) >= 2, 'Polyline must have 1 or more points' + c = self._canvas + c.pathBegin() + c.moveTo(P[0], P[1]) + for i in range(2, len(P), 2): + c.lineTo(P[i], P[i+1]) + if _doClose: + c.pathClose() + c.pathFill() + c.pathStroke() + + def drawEllipse(self, ellipse): + c=self._canvas + c.ellipse(ellipse.cx, ellipse.cy, ellipse.rx,ellipse.ry) + c.fillstrokepath() + + def drawPolygon(self, polygon): + self.drawPolyLine(polygon,_doClose=1) + + def drawString(self, stringObj): + canv = self._canvas + fill = canv.fillColor + if fill is not None: + S = self._tracker.getState() + text_anchor = S['textAnchor'] + fontName = S['fontName'] + fontSize = S['fontSize'] + font = getFont(fontName) + text = stringObj.text + x = stringObj.x + y = stringObj.y + if not text_anchor in ['start','inherited']: + textLen = stringWidth(text, fontName,fontSize) + if text_anchor=='end': + x = x-textLen + elif text_anchor=='middle': + x = x - textLen/2 + else: + raise ValueError, 'bad value for textAnchor '+str(text_anchor) + if getattr(font,'_dynamicFont',None): + if isinstance(text,unicode): text = text.encode('utf8') + canv.drawString(x,y,text) + else: + fc = font + if not isinstance(text,unicode): + try: + text = text.decode('utf8') + except UnicodeDecodeError,e: + i,j = e.args[2:4] + raise UnicodeDecodeError(*(e.args[:4]+('%s\n%s-->%s<--%s' % (e.args[4],text[i-10:i],text[i:j],text[j:j+10]),))) + + FT = unicode2T1(text,[font]+font.substitutionFonts) + n = len(FT) + nm1 = n-1 + wscale = 0.001*fontSize + for i in xrange(n): + f, t = FT[i] + if f!=fc: + canv.setFont(f.fontName,fontSize) + fc = f + canv.drawString(x,y,t) + if i!=nm1: + x += wscale*sum(map(f.widths.__getitem__,map(ord,t))) + if font!=fc: + canv.setFont(fontName,fontSize) + + def drawPath(self, path): + c = self._canvas + if path is EmptyClipPath: + del c._clipPaths[-1] + if c._clipPaths: + P = c._clipPaths[-1] + icp = P.isClipPath + P.isClipPath = 1 + self.drawPath(P) + P.isClipPath = icp + else: + c.clipPathClear() + return + c.pathBegin() + drawFuncs = (c.moveTo, c.lineTo, c.curveTo, c.pathClose) + from reportlab.graphics.shapes import _renderPath + isClosed = _renderPath(path, drawFuncs) + if path.isClipPath: + c.clipPathSet() + c._clipPaths.append(path) + else: + if isClosed: c.pathFill() + c.pathStroke() + +def _setFont(gs,fontName,fontSize): + try: + gs.setFont(fontName,fontSize) + except _renderPM.Error, errMsg: + if errMsg.args[0]!="Can't find font!": raise + #here's where we try to add a font to the canvas + try: + f = getFont(fontName) + if _renderPM._version<='0.98': #added reader arg in 0.99 + _renderPM.makeT1Font(fontName,f.face.findT1File(),f.encoding.vector) + else: + _renderPM.makeT1Font(fontName,f.face.findT1File(),f.encoding.vector,open_and_read) + except: + s1, s2 = map(str,sys.exc_info()[:2]) + raise RenderPMError, "Can't setFont(%s) missing the T1 files?\nOriginally %s: %s" % (fontName,s1,s2) + gs.setFont(fontName,fontSize) + +def _convert2pilp(im): + Image = _getImage() + return im.convert("P", dither=Image.NONE, palette=Image.ADAPTIVE) + +def _saveAsPICT(im,fn,fmt,transparent=None): + im = _convert2pilp(im) + cols, rows = im.size + #s = _renderPM.pil2pict(cols,rows,im.tostring(),im.im.getpalette(),transparent is not None and Color2Hex(transparent) or -1) + s = _renderPM.pil2pict(cols,rows,im.tostring(),im.im.getpalette()) + if not hasattr(fn,'write'): + open(os.path.splitext(fn)[0]+'.'+string.lower(fmt),'wb').write(s) + if os.name=='mac': + from reportlab.lib.utils import markfilename + markfilename(fn,ext='PICT') + else: + fn.write(s) + +BEZIER_ARC_MAGIC = 0.5522847498 #constant for drawing circular arcs w/ Beziers +class PMCanvas: + def __init__(self,w,h,dpi=72,bg=0xffffff,configPIL=None): + '''configPIL dict is passed to image save method''' + scale = dpi/72.0 + w = int(w*scale+0.5) + h = int(h*scale+0.5) + self.__dict__['_gs'] = _renderPM.gstate(w,h,bg=bg) + self.__dict__['_bg'] = bg + self.__dict__['_baseCTM'] = (scale,0,0,scale,0,0) + self.__dict__['_clipPaths'] = [] + self.__dict__['configPIL'] = configPIL + self.__dict__['_dpi'] = dpi + self.ctm = self._baseCTM + + def _drawTimeResize(self,w,h,bg=None): + if bg is None: bg = self._bg + self._drawing.width, self._drawing.height = w, h + A = {'ctm':None, 'strokeWidth':None, 'strokeColor':None, 'lineCap':None, 'lineJoin':None, 'dashArray':None, 'fillColor':None} + gs = self._gs + fN,fS = gs.fontName, gs.fontSize + for k in A.keys(): + A[k] = getattr(gs,k) + del gs, self._gs + gs = self.__dict__['_gs'] = _renderPM.gstate(w,h,bg=bg) + for k in A.keys(): + setattr(self,k,A[k]) + gs.setFont(fN,fS) + + def toPIL(self): + im = _getImage().new('RGB', size=(self._gs.width, self._gs.height)) + im.fromstring(self._gs.pixBuf) + return im + + def saveToFile(self,fn,fmt=None): + im = self.toPIL() + if fmt is None: + if type(fn) is not StringType: + raise ValueError, "Invalid type '%s' for fn when fmt is None" % type(fn) + fmt = os.path.splitext(fn)[1] + if fmt.startswith('.'): fmt = fmt[1:] + configPIL = self.configPIL or {} + fmt = string.upper(fmt) + if fmt in ['GIF']: + im = _convert2pilp(im) + elif fmt in ['PCT','PICT']: + return _saveAsPICT(im,fn,fmt,transparent=configPIL.get('transparent',None)) + elif fmt in ['PNG','TIFF','BMP', 'PPM', 'TIF']: + if fmt=='TIF': fmt = 'TIFF' + if fmt=='PNG': + try: + from PIL import PngImagePlugin + except ImportError: + import PngImagePlugin + elif fmt=='BMP': + try: + from PIL import BmpImagePlugin + except ImportError: + import BmpImagePlugin + elif fmt in ('JPG','JPEG'): + fmt = 'JPEG' + else: + raise RenderPMError,"Unknown image kind %s" % fmt + if fmt=='TIFF': + tc = configPIL.get('transparent',None) + if tc: + from PIL import ImageChops, Image + T = 768*[0] + for o, c in zip((0,256,512), tc.bitmap_rgb()): + T[o+c] = 255 + #if type(fn) is type(''): ImageChops.invert(im.point(T).convert('L').point(255*[0]+[255])).save(fn+'_mask.gif','GIF') + im = Image.merge('RGBA', im.split()+(ImageChops.invert(im.point(T).convert('L').point(255*[0]+[255])),)) + #if type(fn) is type(''): im.save(fn+'_masked.gif','GIF') + for a,d in ('resolution',self._dpi),('resolution unit','inch'): + configPIL[a] = configPIL.get(a,d) + apply(im.save,(fn,fmt),configPIL) + if not hasattr(fn,'write') and os.name=='mac': + from reportlab.lib.utils import markfilename + markfilename(fn,ext=fmt) + + def saveToString(self,fmt='GIF'): + s = getStringIO() + self.saveToFile(s,fmt=fmt) + return s.getvalue() + + def _saveToBMP(self,f): + ''' + Niki Spahiev, , asserts that this is a respectable way to get BMP without PIL + f is a file like object to which the BMP is written + ''' + import struct + gs = self._gs + pix, width, height = gs.pixBuf, gs.width, gs.height + f.write(struct.pack('=2sLLLLLLhh24x','BM',len(pix)+54,0,54,40,width,height,1,24)) + rowb = width * 3 + for o in range(len(pix),0,-rowb): + f.write(pix[o-rowb:o]) + f.write( '\0' * 14 ) + + def setFont(self,fontName,fontSize,leading=None): + _setFont(self._gs,fontName,fontSize) + + def __setattr__(self,name,value): + setattr(self._gs,name,value) + + def __getattr__(self,name): + return getattr(self._gs,name) + + def fillstrokepath(self,stroke=1,fill=1): + if fill: self.pathFill() + if stroke: self.pathStroke() + + def _bezierArcSegmentCCW(self, cx,cy, rx,ry, theta0, theta1): + """compute the control points for a bezier arc with theta1-theta0 <= 90. + Points are computed for an arc with angle theta increasing in the + counter-clockwise (CCW) direction. returns a tuple with starting point + and 3 control points of a cubic bezier curve for the curvto opertator""" + + # Requires theta1 - theta0 <= 90 for a good approximation + assert abs(theta1 - theta0) <= 90 + cos0 = cos(pi*theta0/180.0) + sin0 = sin(pi*theta0/180.0) + x0 = cx + rx*cos0 + y0 = cy + ry*sin0 + + cos1 = cos(pi*theta1/180.0) + sin1 = sin(pi*theta1/180.0) + + x3 = cx + rx*cos1 + y3 = cy + ry*sin1 + + dx1 = -rx * sin0 + dy1 = ry * cos0 + + #from pdfgeom + halfAng = pi*(theta1-theta0)/(2.0 * 180.0) + k = abs(4.0 / 3.0 * (1.0 - cos(halfAng) ) /(sin(halfAng)) ) + x1 = x0 + dx1 * k + y1 = y0 + dy1 * k + + dx2 = -rx * sin1 + dy2 = ry * cos1 + + x2 = x3 - dx2 * k + y2 = y3 - dy2 * k + return ((x0,y0), ((x1,y1), (x2,y2), (x3,y3)) ) + + def bezierArcCCW(self, cx,cy, rx,ry, theta0, theta1): + """return a set of control points for Bezier approximation to an arc + with angle increasing counter clockwise. No requirement on |theta1-theta0| <= 90 + However, it must be true that theta1-theta0 > 0.""" + + # I believe this is also clockwise + # pretty much just like Robert Kern's pdfgeom.BezierArc + angularExtent = theta1 - theta0 + # break down the arc into fragments of <=90 degrees + if abs(angularExtent) <= 90.0: # we just need one fragment + angleList = [(theta0,theta1)] + else: + Nfrag = int( ceil( abs(angularExtent)/90.) ) + fragAngle = float(angularExtent)/ Nfrag # this could be negative + angleList = [] + for ii in range(Nfrag): + a = theta0 + ii * fragAngle + b = a + fragAngle # hmm.. is I wonder if this is precise enought + angleList.append((a,b)) + + ctrlpts = [] + for (a,b) in angleList: + if not ctrlpts: # first time + [(x0,y0), pts] = self._bezierArcSegmentCCW(cx,cy, rx,ry, a,b) + ctrlpts.append(pts) + else: + [(tmpx,tmpy), pts] = self._bezierArcSegmentCCW(cx,cy, rx,ry, a,b) + ctrlpts.append(pts) + return ((x0,y0), ctrlpts) + + def addEllipsoidalArc(self, cx,cy, rx, ry, ang1, ang2): + """adds an ellisesoidal arc segment to a path, with an ellipse centered + on cx,cy and with radii (major & minor axes) rx and ry. The arc is + drawn in the CCW direction. Requires: (ang2-ang1) > 0""" + + ((x0,y0), ctrlpts) = self.bezierArcCCW(cx,cy, rx,ry,ang1,ang2) + + self.lineTo(x0,y0) + for ((x1,y1), (x2,y2),(x3,y3)) in ctrlpts: + self.curveTo(x1,y1,x2,y2,x3,y3) + + def drawCentredString(self, x, y, text, text_anchor='middle'): + if self.fillColor is not None: + textLen = stringWidth(text, self.fontName,self.fontSize) + if text_anchor=='end': + x -= textLen + elif text_anchor=='middle': + x -= textLen/2. + self.drawString(x,y,text) + + def drawRightString(self, text, x, y): + self.drawCentredString(text,x,y,text_anchor='end') + + def line(self,x1,y1,x2,y2): + if self.strokeColor is not None: + self.pathBegin() + self.moveTo(x1,y1) + self.lineTo(x2,y2) + self.pathStroke() + + def rect(self,x,y,width,height,stroke=1,fill=1): + self.pathBegin() + self.moveTo(x, y) + self.lineTo(x+width, y) + self.lineTo(x+width, y + height) + self.lineTo(x, y + height) + self.pathClose() + self.fillstrokepath(stroke=stroke,fill=fill) + + def roundRect(self, x, y, width, height, rx,ry): + """rect(self, x, y, width, height, rx,ry): + Draw a rectangle if rx or rx and ry are specified the corners are + rounded with ellipsoidal arcs determined by rx and ry + (drawn in the counter-clockwise direction)""" + if rx==0: rx = ry + if ry==0: ry = rx + x2 = x + width + y2 = y + height + self.pathBegin() + self.moveTo(x+rx,y) + self.addEllipsoidalArc(x2-rx, y+ry, rx, ry, 270, 360 ) + self.addEllipsoidalArc(x2-rx, y2-ry, rx, ry, 0, 90) + self.addEllipsoidalArc(x+rx, y2-ry, rx, ry, 90, 180) + self.addEllipsoidalArc(x+rx, y+ry, rx, ry, 180, 270) + self.pathClose() + self.fillstrokepath() + + def circle(self, cx, cy, r): + "add closed path circle with center cx,cy and axes r: counter-clockwise orientation" + self.ellipse(cx,cy,r,r) + + def ellipse(self, cx,cy,rx,ry): + """add closed path ellipse with center cx,cy and axes rx,ry: counter-clockwise orientation + (remember y-axis increases downward) """ + self.pathBegin() + # first segment + x0 = cx + rx # (x0,y0) start pt + y0 = cy + + x3 = cx # (x3,y3) end pt of arc + y3 = cy-ry + + x1 = cx+rx + y1 = cy-ry*BEZIER_ARC_MAGIC + + x2 = x3 + rx*BEZIER_ARC_MAGIC + y2 = y3 + self.moveTo(x0, y0) + self.curveTo(x1,y1,x2,y2,x3,y3) + # next segment + x0 = x3 + y0 = y3 + + x3 = cx-rx + y3 = cy + + x1 = cx-rx*BEZIER_ARC_MAGIC + y1 = cy-ry + + x2 = x3 + y2 = cy- ry*BEZIER_ARC_MAGIC + self.curveTo(x1,y1,x2,y2,x3,y3) + # next segment + x0 = x3 + y0 = y3 + + x3 = cx + y3 = cy+ry + + x1 = cx-rx + y1 = cy+ry*BEZIER_ARC_MAGIC + + x2 = cx -rx*BEZIER_ARC_MAGIC + y2 = cy+ry + self.curveTo(x1,y1,x2,y2,x3,y3) + #last segment + x0 = x3 + y0 = y3 + + x3 = cx+rx + y3 = cy + + x1 = cx+rx*BEZIER_ARC_MAGIC + y1 = cy+ry + + x2 = cx+rx + y2 = cy+ry*BEZIER_ARC_MAGIC + self.curveTo(x1,y1,x2,y2,x3,y3) + self.pathClose() + + def saveState(self): + '''do nothing for compatibility''' + pass + + def setFillColor(self,aColor): + self.fillColor = Color2Hex(aColor) + + def setStrokeColor(self,aColor): + self.strokeColor = Color2Hex(aColor) + + restoreState = saveState + + # compatibility routines + def setLineCap(self,cap): + self.lineCap = cap + + def setLineWidth(self,width): + self.strokeWidth = width + +def drawToPMCanvas(d, dpi=72, bg=0xffffff, configPIL=None, showBoundary=rl_config._unset_): + d = renderScaledDrawing(d) + c = PMCanvas(d.width, d.height, dpi=dpi, bg=bg, configPIL=configPIL) + draw(d, c, 0, 0, showBoundary=showBoundary) + return c + +def drawToPIL(d, dpi=72, bg=0xffffff, configPIL=None, showBoundary=rl_config._unset_): + return drawToPMCanvas(d, dpi=dpi, bg=bg, configPIL=configPIL, showBoundary=showBoundary).toPIL() + +def drawToPILP(d, dpi=72, bg=0xffffff, configPIL=None, showBoundary=rl_config._unset_): + Image = _getImage() + im = drawToPIL(d, dpi=dpi, bg=bg, configPIL=configPIL, showBoundary=showBoundary) + return im.convert("P", dither=Image.NONE, palette=Image.ADAPTIVE) + +def drawToFile(d,fn,fmt='GIF', dpi=72, bg=0xffffff, configPIL=None, showBoundary=rl_config._unset_): + '''create a pixmap and draw drawing, d to it then save as a file + configPIL dict is passed to image save method''' + c = drawToPMCanvas(d, dpi=dpi, bg=bg, configPIL=configPIL, showBoundary=showBoundary) + c.saveToFile(fn,fmt) + +def drawToString(d,fmt='GIF', dpi=72, bg=0xffffff, configPIL=None, showBoundary=rl_config._unset_): + s = getStringIO() + drawToFile(d,s,fmt=fmt, dpi=dpi, bg=bg, configPIL=configPIL) + return s.getvalue() + +save = drawToFile + +def test(): + def ext(x): + if x=='tiff': x='tif' + return x + #grab all drawings from the test module and write out. + #make a page of links in HTML to assist viewing. + import os + from reportlab.graphics import testshapes + getAllTestDrawings = testshapes.getAllTestDrawings + drawings = [] + if not os.path.isdir('pmout'): + os.mkdir('pmout') + htmlTop = """renderPM output results + +

renderPM results of output

+ """ + htmlBottom = """ + + """ + html = [htmlTop] + + i = 0 + #print in a loop, with their doc strings + for (drawing, docstring, name) in getAllTestDrawings(doTTF=hasattr(_renderPM,'ft_get_face')): + fnRoot = 'renderPM%d' % i + if 1 or i==10: + w = int(drawing.width) + h = int(drawing.height) + html.append('

Drawing %s %d

\n
%s
' % (name, i, docstring)) + + for k in ['gif','tiff', 'png', 'jpg', 'pct']: + if k in ['gif','png','jpg','pct']: + html.append('

%s format

\n' % string.upper(k)) + try: + filename = '%s.%s' % (fnRoot, ext(k)) + fullpath = os.path.join('pmout', filename) + if os.path.isfile(fullpath): + os.remove(fullpath) + if k=='pct': + from reportlab.lib.colors import white + drawToFile(drawing,fullpath,fmt=k,configPIL={'transparent':white}) + else: + drawToFile(drawing,fullpath,fmt=k) + if k in ['gif','png','jpg']: + html.append('
\n' % filename) + print 'wrote',fullpath + except AttributeError: + print 'Problem drawing %s file'%k + raise + if os.environ.get('RL_NOEPSPREVIEW','0')=='1': drawing.__dict__['preview'] = 0 + drawing.save(formats=['eps','pdf'],outDir='pmout',fnRoot=fnRoot) + i = i + 1 + #if i==10: break + html.append(htmlBottom) + htmlFileName = os.path.join('pmout', 'index.html') + open(htmlFileName, 'w').writelines(html) + if sys.platform=='mac': + from reportlab.lib.utils import markfilename + markfilename(htmlFileName,ext='HTML') + print 'wrote %s' % htmlFileName + +if __name__=='__main__': + test() diff --git a/bin/reportlab/graphics/renderPS.py b/bin/reportlab/graphics/renderPS.py new file mode 100644 index 00000000000..b6e11828fc3 --- /dev/null +++ b/bin/reportlab/graphics/renderPS.py @@ -0,0 +1,871 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/renderPS.py +__version__=''' $Id: renderPS.py 2808 2006-03-15 16:47:27Z rgbecker $ ''' +import string, types +from reportlab.pdfbase.pdfmetrics import getFont, stringWidth # for font info +from reportlab.lib.utils import fp_str, getStringIO +from reportlab.lib.colors import black +from reportlab.graphics.renderbase import Renderer, StateTracker, getStateDelta, renderScaledDrawing +from reportlab.graphics.shapes import STATE_DEFAULTS +import math +from types import StringType +from operator import getitem +from reportlab import rl_config + + +# we need to create encoding vectors for each font we use, or they will +# come out in Adobe's old StandardEncoding, which NOBODY uses. +PS_WinAnsiEncoding=""" +/RE { %def + findfont begin + currentdict dup length dict begin + { %forall + 1 index /FID ne { def } { pop pop } ifelse + } forall + /FontName exch def dup length 0 ne { %if + /Encoding Encoding 256 array copy def + 0 exch { %forall + dup type /nametype eq { %ifelse + Encoding 2 index 2 index put + pop 1 add + }{ %else + exch pop + } ifelse + } forall + } if pop + currentdict dup end end + /FontName get exch definefont pop +} bind def + +/WinAnsiEncoding [ + 39/quotesingle 96/grave 128/euro 130/quotesinglbase/florin/quotedblbase + /ellipsis/dagger/daggerdbl/circumflex/perthousand + /Scaron/guilsinglleft/OE 145/quoteleft/quoteright + /quotedblleft/quotedblright/bullet/endash/emdash + /tilde/trademark/scaron/guilsinglright/oe/dotlessi + 159/Ydieresis 164/currency 166/brokenbar 168/dieresis/copyright + /ordfeminine 172/logicalnot 174/registered/macron/ring + 177/plusminus/twosuperior/threesuperior/acute/mu + 183/periodcentered/cedilla/onesuperior/ordmasculine + 188/onequarter/onehalf/threequarters 192/Agrave/Aacute + /Acircumflex/Atilde/Adieresis/Aring/AE/Ccedilla + /Egrave/Eacute/Ecircumflex/Edieresis/Igrave/Iacute + /Icircumflex/Idieresis/Eth/Ntilde/Ograve/Oacute + /Ocircumflex/Otilde/Odieresis/multiply/Oslash + /Ugrave/Uacute/Ucircumflex/Udieresis/Yacute/Thorn + /germandbls/agrave/aacute/acircumflex/atilde/adieresis + /aring/ae/ccedilla/egrave/eacute/ecircumflex + /edieresis/igrave/iacute/icircumflex/idieresis + /eth/ntilde/ograve/oacute/ocircumflex/otilde + /odieresis/divide/oslash/ugrave/uacute/ucircumflex + /udieresis/yacute/thorn/ydieresis +] def + +""" + +class PSCanvas: + def __init__(self,size=(300,300), PostScriptLevel=2): + self.width, self.height = size + xtraState = [] + self._xtraState_push = xtraState.append + self._xtraState_pop = xtraState.pop + self.comments = 0 + self.code = [] + self._sep = '\n' + self._strokeColor = self._fillColor = self._lineWidth = \ + self._font = self._fontSize = self._lineCap = \ + self._lineJoin = self._color = None + + + self._fontsUsed = [] # track them as we go + + self.setFont(STATE_DEFAULTS['fontName'],STATE_DEFAULTS['fontSize']) + self.setStrokeColor(STATE_DEFAULTS['strokeColor']) + self.setLineCap(2) + self.setLineJoin(0) + self.setLineWidth(1) + + self.PostScriptLevel=PostScriptLevel + + def comment(self,msg): + if self.comments: self.code.append('%'+msg) + + def drawImage(self, image, x1,y1, x2=None,y2=None): # Postscript Level2 version + # select between postscript level 1 or level 2 + if PostScriptLevel==1: + self._drawImageLevel1(image, x1,y1, x2=None,y2=None) + elif PostScriptLevel == 2 : + self._drawImageLevel2(image, x1,y1, x2=None,y2=None) + else : + raise 'PostScriptLevelException' + + def clear(self): + self.code.append('showpage') # ugh, this makes no sense oh well. + + def save(self,f=None): + if not hasattr(f,'write'): + file = open(f,'wb') + else: + file = f + if self.code[-1]!='showpage': self.clear() + self.code.insert(0,'''\ +%%!PS-Adobe-3.0 EPSF-3.0 +%%%%BoundingBox: 0 0 %d %d +%%%% Initialization: +/m {moveto} bind def +/l {lineto} bind def +/c {curveto} bind def + +%s +''' % (self.width,self.height, PS_WinAnsiEncoding)) + + # for each font used, reencode the vectors + fontReencode = [] + for fontName in self._fontsUsed: + fontReencode.append('WinAnsiEncoding /%s /%s RE' % (fontName, fontName)) + self.code.insert(1, string.join(fontReencode, self._sep)) + + file.write(string.join(self.code,self._sep)) + if file is not f: + file.close() + from reportlab.lib.utils import markfilename + markfilename(f,creatorcode='XPR3',filetype='EPSF') + + def saveState(self): + self._xtraState_push((self._fontCodeLoc,)) + self.code.append('gsave') + + def restoreState(self): + self.code.append('grestore') + self._fontCodeLoc, = self._xtraState_pop() + + def stringWidth(self, s, font=None, fontSize=None): + """Return the logical width of the string if it were drawn + in the current font (defaults to self.font).""" + font = font or self._font + fontSize = fontSize or self._fontSize + return stringWidth(s, font, fontSize) + + def setLineCap(self,v): + if self._lineCap!=v: + self._lineCap = v + self.code.append('%d setlinecap'%v) + + def setLineJoin(self,v): + if self._lineJoin!=v: + self._lineJoin = v + self.code.append('%d setlinejoin'%v) + + def setDash(self, array=[], phase=0): + """Two notations. pass two numbers, or an array and phase""" + # copied and modified from reportlab.canvas + psoperation = "setdash" + if type(array) == types.IntType or type(array) == types.FloatType: + self._code.append('[%s %s] 0 %s' % (array, phase, psoperation)) + elif type(array) == types.ListType or type(array) == types.TupleType: + assert phase >= 0, "phase is a length in user space" + textarray = string.join(map(str, array)) + self.code.append('[%s] %s %s' % (textarray, phase, psoperation)) + + def setStrokeColor(self, color): + self._strokeColor = color + self.setColor(color) + + def setColor(self, color): + if self._color!=color: + self._color = color + if color: + if hasattr(color, "cyan"): + self.code.append('%s setcmykcolor' % fp_str(color.cyan, color.magenta, color.yellow, color.black)) + else: + self.code.append('%s setrgbcolor' % fp_str(color.red, color.green, color.blue)) + + def setFillColor(self, color): + self._fillColor = color + self.setColor(color) + + def setLineWidth(self, width): + if width != self._lineWidth: + self._lineWidth = width + self.code.append('%s setlinewidth' % width) + + def setFont(self,font,fontSize,leading=None): + if self._font!=font or self._fontSize!=fontSize: + self._fontCodeLoc = len(self.code) + self._font = font + self._fontSize = fontSize + self.code.append('') + + def line(self, x1, y1, x2, y2): + if self._strokeColor != None: + self.setColor(self._strokeColor) + self.code.append('%s m %s l stroke' % (fp_str(x1, y1), fp_str(x2, y2))) + + def _escape(self, s): + ''' + return a copy of string s with special characters in postscript strings + escaped with backslashes. + Have not handled characters that are converted normally in python strings + i.e. \n -> newline + ''' + str = string.replace(s, chr(0x5C), r'\\' ) + str = string.replace(str, '(', '\(' ) + str = string.replace(str, ')', '\)') + return str + + def drawString(self, x, y, s, angle=0): + if self._fillColor != None: + if not self.code[self._fontCodeLoc]: + psName = getFont(self._font).face.name + self.code[self._fontCodeLoc]='(%s) findfont %s scalefont setfont' % (psName,fp_str(self._fontSize)) + if psName not in self._fontsUsed: + self._fontsUsed.append(psName) + self.setColor(self._fillColor) + s = self._escape(s) +## before inverting... +## if angle == 0 : # do special case of angle = 0 first. Avoids a bunch of gsave/grestore ops +## self.code.append('%s m 1 -1 scale (%s) show 1 -1 scale' % (fp_str(x,y),s)) +## else : # general case, rotated text +## self.code.append('gsave %s %s translate %s rotate' % (x,y,angle)) +## self.code.append('0 0 m 1 -1 scale (%s) show' % s) +## self.code.append('grestore') + if angle == 0 : # do special case of angle = 0 first. Avoids a bunch of gsave/grestore ops + self.code.append('%s m (%s) show ' % (fp_str(x,y),s)) + else : # general case, rotated text + self.code.append('gsave %s %s translate %s rotate' % (x,y,angle)) + self.code.append('0 0 m (%s) show' % s) + self.code.append('grestore') + + def drawCentredString(self, x, y, text, text_anchor='middle'): + if self._fillColor is not None: + textLen = stringWidth(text, self._font,self._fontSize) + if text_anchor=='end': + x -= textLen + elif text_anchor=='middle': + x -= textLen/2. + self.drawString(x,y,text) + + def drawRightString(self, text, x, y): + self.drawCentredString(text,x,y,text_anchor='end') + + def drawCurve(self, x1, y1, x2, y2, x3, y3, x4, y4, closed=0): + codeline = '%s m %s curveto' + data = (fp_str(x1, y1), fp_str(x2, y2, x3, y3, x4, y4)) + if self._fillColor != None: + self.setColor(self._fillColor) + self.code.append((codeline % data) + ' eofill') + if self._strokeColor != None: + self.setColor(self._strokeColor) + self.code.append((codeline % data) + + ((closed and ' closepath') or '') + + ' stroke') + + ######################################################################################## + + def rect(self, x1,y1, x2,y2, stroke=1, fill=1): + "Draw a rectangle between x1,y1, and x2,y2" + # Path is drawn in counter-clockwise direction" + + x1, x2 = min(x1,x2), max(x1, x2) # from piddle.py + y1, y2 = min(y1,y2), max(y1, y2) + self.polygon(((x1,y1),(x2,y1),(x2,y2),(x1,y2)), closed=1, stroke=stroke, fill = fill) + + def roundRect(self, x1,y1, x2,y2, rx=8, ry=8): + """Draw a rounded rectangle between x1,y1, and x2,y2, + with corners inset as ellipses with x radius rx and y radius ry. + These should have x10, and ry>0.""" + # Path is drawn in counter-clockwise direction + + x1, x2 = min(x1,x2), max(x1, x2) # from piddle.py + y1, y2 = min(y1,y2), max(y1, y2) + + # Note: arcto command draws a line from current point to beginning of arc + # save current matrix, translate to center of ellipse, scale by rx ry, and draw + # a circle of unit radius in counterclockwise dir, return to original matrix + # arguments are (cx, cy, rx, ry, startAngle, endAngle) + ellipsePath = 'matrix currentmatrix %s %s translate %s %s scale 0 0 1 %s %s arc setmatrix' + + # choice between newpath and moveTo beginning of arc + # go with newpath for precision, does this violate any assumptions in code??? + rrCode = ['newpath'] # Round Rect code path + # upper left corner ellipse is first + rrCode.append(ellipsePath % (x1+rx, y1+ry, rx, -ry, 90, 180)) + rrCode.append(ellipsePath % (x1+rx, y2-ry, rx, -ry, 180, 270)) + rrCode.append(ellipsePath % (x2-rx, y2-ry, rx, -ry, 270, 360)) + rrCode.append(ellipsePath % (x2-rx, y1+ry, rx, -ry, 0, 90) ) + rrCode.append('closepath') + + self._fillAndStroke(rrCode) + + def ellipse(self, x1,y1, x2,y2): + """Draw an orthogonal ellipse inscribed within the rectangle x1,y1,x2,y2. + These should have x1= 0: + arc='arc' + else: + arc='arcn' + data = (x,y, xScale, yScale, startAng, startAng+extent, arc) + + return codeline % data + + def polygon(self, p, closed=0, stroke=1, fill=1): + assert len(p) >= 2, 'Polygon must have 2 or more points' + + start = p[0] + p = p[1:] + + polyCode = [] + polyCode.append("%s m" % fp_str(start)) + for point in p: + polyCode.append("%s l" % fp_str(point)) + if closed: + polyCode.append("closepath") + + self._fillAndStroke(polyCode,stroke=stroke,fill=fill) + + def lines(self, lineList, color=None, width=None): + if self._strokeColor != None: + self._setColor(self._strokeColor) + codeline = '%s m %s l stroke' + for line in lineList: + self.code.append(codeline % (fp_str(line[0]),fp_str(line[1]))) + + def moveTo(self,x,y): + self.code.append('%s m' % fp_str(x, y)) + + def lineTo(self,x,y): + self.code.append('%s l' % fp_str(x, y)) + + def curveTo(self,x1,y1,x2,y2,x3,y3): + self.code.append('%s c' % fp_str(x1,y1,x2,y2,x3,y3)) + + def closePath(self): + self.code.append('closepath') + + def polyLine(self, p): + assert len(p) >= 1, 'Polyline must have 1 or more points' + if self._strokeColor != None: + self.setColor(self._strokeColor) + self.moveTo(p[0][0], p[0][1]) + for t in p[1:]: + self.lineTo(t[0], t[1]) + self.code.append('stroke') + + + def drawFigure(self, partList, closed=0): + figureCode = [] + first = 1 + + for part in partList: + op = part[0] + args = list(part[1:]) + + if op == figureLine: + if first: + first = 0 + figureCode.append("%s m" % fp_str(args[:2])) + else: + figureCode.append("%s l" % fp_str(args[:2])) + figureCode.append("%s l" % fp_str(args[2:])) + + elif op == figureArc: + first = 0 + x1,y1,x2,y2,startAngle,extent = args[:6] + figureCode.append(self._genArcCode(x1,y1,x2,y2,startAngle,extent)) + + elif op == figureCurve: + if first: + first = 0 + figureCode.append("%s m" % fp_str(args[:2])) + else: + figureCode.append("%s l" % fp_str(args[:2])) + figureCode.append("%s curveto" % fp_str(args[2:])) + else: + raise TypeError, "unknown figure operator: "+op + + if closed: + figureCode.append("closepath") + self._fillAndStroke(figureCode) + + def _fillAndStroke(self,code,clip=0,fill=1,stroke=1): + fill = self._fillColor and fill + stroke = self._strokeColor and stroke + if fill or stroke or clip: + self.code.extend(code) + if fill: + if stroke or clip: self.code.append("gsave") + self.setColor(self._fillColor) + self.code.append("eofill") + if stroke or clip: self.code.append("grestore") + if stroke: + if clip: self.code.append("gsave") + self.setColor(self._strokeColor) + self.code.append("stroke") + if clip: self.code.append("grestore") + if clip: + self.code.append("clip") + self.code.append("newpath") + + + def translate(self,x,y): + self.code.append('%s translate' % fp_str(x,y)) + + def scale(self,x,y): + self.code.append('%s scale' % fp_str(x,y)) + + def transform(self,a,b,c,d,e,f): + self.code.append('[%s] concat' % fp_str(a,b,c,d,e,f)) + + def _drawTimeResize(self,w,h): + '''if this is used we're probably in the wrong world''' + self.width, self.height = w, h + + ############################################################################################ + # drawImage(self. image, x1, y1, x2=None, y2=None) is now defined by either _drawImageLevel1 + # ._drawImageLevel2, the choice is made in .__init__ depending on option + def _drawImageLevel1(self, image, x1, y1, x2=None,y2=None): + # Postscript Level1 version available for fallback mode when Level2 doesn't work + """drawImage(self,image,x1,y1,x2=None,y2=None) : If x2 and y2 are ommitted, they are + calculated from image size. (x1,y1) is upper left of image, (x2,y2) is lower right of + image in piddle coordinates.""" + # For now let's start with 24 bit RGB images (following piddlePDF again) + component_depth = 8 + myimage = image.convert('RGB') + imgwidth, imgheight = myimage.size + if not x2: + x2 = imgwidth + x1 + if not y2: + y2 = y1 + imgheight + drawwidth = x2 - x1 + drawheight = y2 - y1 + #print 'Image size (%d, %d); Draw size (%d, %d)' % (imgwidth, imgheight, drawwidth, drawheight) + # now I need to tell postscript how big image is + + # "image operators assume that they receive sample data from + # their data source in x-axis major index order. The coordinate + # of the lower-left corner of the first sample is (0,0), of the + # second (1,0) and so on" -PS2 ref manual p. 215 + # + # The ImageMatrix maps unit squre of user space to boundary of the source image + # + + # The CurrentTransformationMatrix (CTM) maps the unit square of + # user space to the rect...on the page that is to receive the + # image. A common ImageMatrix is [width 0 0 -height 0 height] + # (for a left to right, top to bottom image ) + + # first let's map the user coordinates start at offset x1,y1 on page + + self.code.extend([ + 'gsave', + '%s %s translate' % (x1,-y1 - drawheight), # need to start are lower left of image + '%s %s scale' % (drawwidth,drawheight), + '/scanline %d 3 mul string def' % imgwidth # scanline by multiples of image width + ]) + + # now push the dimensions and depth info onto the stack + # and push the ImageMatrix to map the source to the target rectangle (see above) + # finally specify source (PS2 pp. 225 ) and by exmample + self.code.extend([ + '%s %s %s' % (imgwidth, imgheight, component_depth), + '[%s %s %s %s %s %s]' % (imgwidth, 0, 0, -imgheight, 0, imgheight), + '{ currentfile scanline readhexstring pop } false 3', + 'colorimage ' + ]) + + # data source output--now we just need to deliver a hex encode + # series of lines of the right overall size can follow + # piddlePDF again + + rawimage = myimage.tostring() + assert(len(rawimage) == imgwidth*imgheight, 'Wrong amount of data for image') + #compressed = zlib.compress(rawimage) # no zlib at moment + hex_encoded = self._AsciiHexEncode(rawimage) + + # write in blocks of 78 chars per line + outstream = getStringIO(hex_encoded) + + dataline = outstream.read(78) + while dataline <> "": + self.code.append(dataline) + dataline= outstream.read(78) + self.code.append('% end of image data') # for clarity + self.code.append('grestore') # return coordinates to normal + + # end of drawImage + def _AsciiHexEncode(self, input): # also based on piddlePDF + "Helper function used by images" + output = getStringIO() + for char in input: + output.write('%02x' % ord(char)) + return output.getvalue() + + def _drawImageLevel2(self, image, x1,y1, x2=None,y2=None): # Postscript Level2 version + '''At present we're handling only PIL''' + ### what sort of image are we to draw + if image.mode=='L' : + imBitsPerComponent = 8 + imNumComponents = 1 + myimage = image + elif image.mode == '1': + myimage = image.convert('L') + imNumComponents = 1 + myimage = image + else : + myimage = image.convert('RGB') + imNumComponents = 3 + imBitsPerComponent = 8 + + imwidth, imheight = myimage.size + if not x2: + x2 = imwidth + x1 + if not y2: + y2 = y1 + imheight + drawwidth = x2 - x1 + drawheight = y2 - y1 + self.code.extend([ + 'gsave', + '%s %s translate' % (x1,-y1 - drawheight), # need to start are lower left of image + '%s %s scale' % (drawwidth,drawheight)]) + + if imNumComponents == 3 : + self.code.append('/DeviceRGB setcolorspace') + elif imNumComponents == 1 : + self.code.append('/DeviceGray setcolorspace') + # create the image dictionary + self.code.append(""" +<< +/ImageType 1 +/Width %d /Height %d %% dimensions of source image +/BitsPerComponent %d""" % (imwidth, imheight, imBitsPerComponent) ) + + if imNumComponents == 1: + self.code.append('/Decode [0 1]') + if imNumComponents == 3: + self.code.append('/Decode [0 1 0 1 0 1] %% decode color values normally') + + self.code.extend([ '/ImageMatrix [%s 0 0 %s 0 %s]' % (imwidth, -imheight, imheight), + '/DataSource currentfile /ASCIIHexDecode filter', + '>> % End image dictionary', + 'image']) + # after image operator just need to dump image dat to file as hexstring + rawimage = myimage.tostring() + assert(len(rawimage) == imwidth*imheight, 'Wrong amount of data for image') + #compressed = zlib.compress(rawimage) # no zlib at moment + hex_encoded = self._AsciiHexEncode(rawimage) + + # write in blocks of 78 chars per line + outstream = getStringIO(hex_encoded) + + dataline = outstream.read(78) + while dataline <> "": + self.code.append(dataline) + dataline= outstream.read(78) + self.code.append('> % end of image data') # > is EOD for hex encoded filterfor clarity + self.code.append('grestore') # return coordinates to normal + +# renderpdf - draws them onto a canvas +"""Usage: + from reportlab.graphics import renderPS + renderPS.draw(drawing, canvas, x, y) +Execute the script to see some test drawings.""" +from shapes import * + +# hack so we only get warnings once each +#warnOnce = WarnOnce() + +# the main entry point for users... +def draw(drawing, canvas, x=0, y=0, showBoundary=rl_config.showBoundary): + """As it says""" + R = _PSRenderer() + R.draw(renderScaledDrawing(drawing), canvas, x, y, showBoundary=showBoundary) + +def _pointsFromList(L): + ''' + given a list of coordinates [x0, y0, x1, y1....] + produce a list of points [(x0,y0), (y1,y0),....] + ''' + P=[] + for i in range(0,len(L),2): + P.append((L[i],L[i+1])) + return P + +class _PSRenderer(Renderer): + """This draws onto a EPS document. It needs to be a class + rather than a function, as some EPS-specific state tracking is + needed outside of the state info in the SVG model.""" + + def __init__(self): + self._tracker = StateTracker() + + def drawNode(self, node): + """This is the recursive method called for each node + in the tree""" + self._canvas.comment('begin node %s'%`node`) + color = self._canvas._color + if not (isinstance(node, Path) and node.isClipPath): + self._canvas.saveState() + + #apply state changes + deltas = getStateDelta(node) + self._tracker.push(deltas) + self.applyStateChanges(deltas, {}) + + #draw the object, or recurse + self.drawNodeDispatcher(node) + + rDeltas = self._tracker.pop() + if not (isinstance(node, Path) and node.isClipPath): + self._canvas.restoreState() + self._canvas.comment('end node %s'%`node`) + self._canvas._color = color + + #restore things we might have lost (without actually doing anything). + for k, v in rDeltas.items(): + if self._restores.has_key(k): + setattr(self._canvas,self._restores[k],v) + +## _restores = {'stroke':'_stroke','stroke_width': '_lineWidth','stroke_linecap':'_lineCap', +## 'stroke_linejoin':'_lineJoin','fill':'_fill','font_family':'_font', +## 'font_size':'_fontSize'} + _restores = {'strokeColor':'_strokeColor','strokeWidth': '_lineWidth','strokeLineCap':'_lineCap', + 'strokeLineJoin':'_lineJoin','fillColor':'_fillColor','fontName':'_font', + 'fontSize':'_fontSize'} + + def drawRect(self, rect): + if rect.rx == rect.ry == 0: + #plain old rectangle + self._canvas.rect( + rect.x, rect.y, + rect.x+rect.width, rect.y+rect.height) + else: + #cheat and assume ry = rx; better to generalize + #pdfgen roundRect function. TODO + self._canvas.roundRect( + rect.x, rect.y, + rect.x+rect.width, rect.y+rect.height, rect.rx, rect.ry + ) + + def drawLine(self, line): + if self._canvas._strokeColor: + self._canvas.line(line.x1, line.y1, line.x2, line.y2) + + def drawCircle(self, circle): + self._canvas.circle( circle.cx, circle.cy, circle.r) + + def drawWedge(self, wedge): + yradius, radius1, yradius1 = wedge._xtraRadii() + if (radius1==0 or radius1 is None) and (yradius1==0 or yradius1 is None): + startangledegrees = wedge.startangledegrees + endangledegrees = wedge.endangledegrees + centerx= wedge.centerx + centery = wedge.centery + radius = wedge.radius + extent = endangledegrees - startangledegrees + self._canvas.drawArc(centerx-radius, centery-yradius, centerx+radius, centery+yradius, + startangledegrees, extent, fromcenter=1) + else: + self.drawPolygon(wedge.asPolygon()) + + def drawPolyLine(self, p): + if self._canvas._strokeColor: + self._canvas.polyLine(_pointsFromList(p.points)) + + def drawEllipse(self, ellipse): + #need to convert to pdfgen's bounding box representation + x1 = ellipse.cx - ellipse.rx + x2 = ellipse.cx + ellipse.rx + y1 = ellipse.cy - ellipse.ry + y2 = ellipse.cy + ellipse.ry + self._canvas.ellipse(x1,y1,x2,y2) + + def drawPolygon(self, p): + self._canvas.polygon(_pointsFromList(p.points), closed=1) + + def drawString(self, stringObj): + if self._canvas._fillColor: + S = self._tracker.getState() + text_anchor, x, y, text = S['textAnchor'], stringObj.x,stringObj.y,stringObj.text + if not text_anchor in ['start','inherited']: + font, fontSize = S['fontName'], S['fontSize'] + textLen = stringWidth(text, font,fontSize) + if text_anchor=='end': + x = x-textLen + elif text_anchor=='middle': + x = x - textLen/2 + else: + raise ValueError, 'bad value for text_anchor '+str(text_anchor) + self._canvas.drawString(x,y,text) + + def drawPath(self, path): + from reportlab.graphics.shapes import _renderPath + c = self._canvas + drawFuncs = (c.moveTo, c.lineTo, c.curveTo, c.closePath) + isClosed = _renderPath(path, drawFuncs) + if not isClosed: + c._fillColor = None + c._fillAndStroke([], clip=path.isClipPath) + + def applyStateChanges(self, delta, newState): + """This takes a set of states, and outputs the operators + needed to set those properties""" + for key, value in delta.items(): + if key == 'transform': + self._canvas.transform(value[0], value[1], value[2], + value[3], value[4], value[5]) + elif key == 'strokeColor': + #this has different semantics in PDF to SVG; + #we always have a color, and either do or do + #not apply it; in SVG one can have a 'None' color + self._canvas.setStrokeColor(value) + elif key == 'strokeWidth': + self._canvas.setLineWidth(value) + elif key == 'strokeLineCap': #0,1,2 + self._canvas.setLineCap(value) + elif key == 'strokeLineJoin': + self._canvas.setLineJoin(value) + elif key == 'strokeDashArray': + if value: + self._canvas.setDash(value) + else: + self._canvas.setDash() +## elif key == 'stroke_opacity': +## warnOnce('Stroke Opacity not supported yet') + elif key == 'fillColor': + #this has different semantics in PDF to SVG; + #we always have a color, and either do or do + #not apply it; in SVG one can have a 'None' color + self._canvas.setFillColor(value) +## elif key == 'fill_rule': +## warnOnce('Fill rules not done yet') +## elif key == 'fill_opacity': +## warnOnce('Fill opacity not done yet') + elif key in ['fontSize', 'fontName']: + # both need setting together in PDF + # one or both might be in the deltas, + # so need to get whichever is missing + fontname = delta.get('fontName', self._canvas._font) + fontsize = delta.get('fontSize', self._canvas._fontSize) + self._canvas.setFont(fontname, fontsize) + + def drawImage(self, image): + from reportlab.lib.utils import ImageReader + im = ImageReader(image.path) + x0 = image.x + y0 = image.y + x1 = image.width + if x1 is not None: x1 += x0 + y1 = image.height + if y1 is not None: y1 += y0 + self._canvas.drawImage(im._image,x0,y0,x1,y1) + +def drawToFile(d,fn, showBoundary=rl_config.showBoundary): + d = renderScaledDrawing(d) + c = PSCanvas((d.width,d.height)) + draw(d, c, 0, 0, showBoundary=showBoundary) + c.save(fn) + +def drawToString(d, showBoundary=rl_config.showBoundary): + "Returns a PS as a string in memory, without touching the disk" + s = getStringIO() + drawToFile(d, s, showBoundary=showBoundary) + return s.getvalue() + +######################################################### +# +# test code. First, defin a bunch of drawings. +# Routine to draw them comes at the end. +# +######################################################### +def test(outdir='epsout'): + import os + # print all drawings and their doc strings from the test + # file + if not os.path.isdir(outdir): + os.mkdir(outdir) + #grab all drawings from the test module + import testshapes + drawings = [] + + for funcname in dir(testshapes): + #if funcname[0:11] == 'getDrawing2': + # print 'hacked to only show drawing 2' + if funcname[0:10] == 'getDrawing': + drawing = eval('testshapes.' + funcname + '()') #execute it + docstring = eval('testshapes.' + funcname + '.__doc__') + drawings.append((drawing, docstring)) + + i = 0 + for (d, docstring) in drawings: + filename = outdir + os.sep + 'renderPS_%d.eps'%i + drawToFile(d,filename) + print 'saved', filename + i = i + 1 + +if __name__=='__main__': + import sys + if len(sys.argv)>1: + outdir = sys.argv[1] + else: + outdir = 'epsout' + test(outdir) diff --git a/bin/reportlab/graphics/renderSVG.py b/bin/reportlab/graphics/renderSVG.py new file mode 100644 index 00000000000..73aaddfc16d --- /dev/null +++ b/bin/reportlab/graphics/renderSVG.py @@ -0,0 +1,828 @@ +"""An experimental SVG renderer for the ReportLab graphics framework. + +This will create SVG code from the ReportLab Graphics API (RLG). +To read existing SVG code and convert it into ReportLab graphics +objects download the svglib module here: + + http://python.net/~gherman/#svglib +""" + +import math, string, types, sys, os +from types import StringType +from operator import getitem + +from reportlab.pdfbase.pdfmetrics import stringWidth # for font info +from reportlab.lib.utils import fp_str +from reportlab.lib.colors import black +from reportlab.graphics.renderbase import StateTracker, getStateDelta, Renderer, renderScaledDrawing +from reportlab.graphics.shapes import STATE_DEFAULTS, Path, UserNode +from reportlab.graphics.shapes import * # (only for test0) +from reportlab import rl_config +from reportlab.lib.utils import getStringIO + +from xml.dom import getDOMImplementation + + +### some constants ### + +sin = math.sin +cos = math.cos +pi = math.pi + +LINE_STYLES = 'stroke-width stroke-linecap stroke fill stroke-dasharray' +TEXT_STYLES = 'font-family font-size' + + +### top-level user function ### + +def drawToString(d, showBoundary=rl_config.showBoundary): + "Returns a SVG as a string in memory, without touching the disk" + s = getStringIO() + drawToFile(d, s, showBoundary=showBoundary) + return s.getvalue() + +def drawToFile(d, fn, showBoundary=rl_config.showBoundary): + d = renderScaledDrawing(d) + c = SVGCanvas((d.width, d.height)) + draw(d, c, 0, 0, showBoundary=showBoundary) + c.save(fn) + + +def draw(drawing, canvas, x=0, y=0, showBoundary=rl_config.showBoundary): + """As it says.""" + r = _SVGRenderer() + r.draw(renderScaledDrawing(drawing), canvas, x, y, showBoundary=showBoundary) + + +### helper functions ### + +def _pointsFromList(L): + """ + given a list of coordinates [x0, y0, x1, y1....] + produce a list of points [(x0,y0), (y1,y0),....] + """ + + P=[] + for i in range(0,len(L), 2): + P.append((L[i], L[i+1])) + + return P + + +def transformNode(doc, newTag, node=None, **attrDict): + """Transform a DOM node into new node and copy selected attributes. + + Creates a new DOM node with tag name 'newTag' for document 'doc' + and copies selected attributes from an existing 'node' as provided + in 'attrDict'. The source 'node' can be None. Attribute values will + be converted to strings. + + E.g. + + n = transformNode(doc, "node1", x="0", y="1") + -> DOM node for + + n = transformNode(doc, "node1", x=0, y=1+1) + -> DOM node for + + n = transformNode(doc, "node1", node0, x="x0", y="x0", zoo=bar()) + -> DOM node for + """ + + newNode = doc.createElement(newTag) + for newAttr, attr in attrDict.items(): + sattr = str(attr) + if not node: + newNode.setAttribute(newAttr, sattr) + else: + attrVal = node.getAttribute(sattr) + newNode.setAttribute(newAttr, attrVal or sattr) + + return newNode + + +### classes ### + +class SVGCanvas: + def __init__(self, size=(300,300)): + self.verbose = 0 + self.width, self.height = self.size = size + # self.height = size[1] + self.code = [] + self.style = {} + self.path = '' + self._strokeColor = self._fillColor = self._lineWidth = \ + self._font = self._fontSize = self._lineCap = \ + self._lineJoin = self._color = None + + implementation = getDOMImplementation('minidom') + self.doc = implementation.createDocument(None, "svg", None) + self.svg = self.doc.documentElement + self.svg.setAttribute("width", str(size[0])) + self.svg.setAttribute("height", str(self.height)) + + title = self.doc.createElement('title') + text = self.doc.createTextNode('...') + title.appendChild(text) + self.svg.appendChild(title) + + desc = self.doc.createElement('desc') + text = self.doc.createTextNode('...') + desc.appendChild(text) + self.svg.appendChild(desc) + + self.setFont(STATE_DEFAULTS['fontName'], STATE_DEFAULTS['fontSize']) + self.setStrokeColor(STATE_DEFAULTS['strokeColor']) + self.setLineCap(2) + self.setLineJoin(0) + self.setLineWidth(1) + + # Add a rectangular clipping path identical to view area. + clipPath = transformNode(self.doc, "clipPath", id="clip") + clipRect = transformNode(self.doc, "rect", x=0, y=0, + width=self.width, height=self.height) + clipPath.appendChild(clipRect) + self.svg.appendChild(clipPath) + + self.groupTree = transformNode(self.doc, "g", + id="group", + transform="scale(1,-1) translate(0,-%d)" % self.height, + style="clip-path: url(#clip)") + self.svg.appendChild(self.groupTree) + self.currGroup = self.groupTree + + + def save(self, f=None): + if type(f) is StringType: + file = open(f, 'w') + else: + file = f + + file.write("""\ + +\n""") + + # use = self.doc.createElement('use') + # use.setAttribute("xlink:href", "#group") + # use.setAttribute("transform", "scale(1, -1)") + # self.svg.appendChild(use) + + result = self.svg.toprettyxml(indent=" ") + file.write(result) + + if file is not f: + file.close() + + + ### helpers ### + + def NOTUSED_stringWidth(self, s, font=None, fontSize=None): + """Return the logical width of the string if it were drawn + in the current font (defaults to self.font). + """ + + font = font or self._font + fontSize = fontSize or self._fontSize + + return stringWidth(s, font, fontSize) + + + def _formatStyle(self, include=''): + str = '' + include = string.split(include) + keys = self.style.keys() + if include: + #2.1-safe version of the line below follows: + #keys = filter(lambda k: k in include, keys) + tmp = [] + for word in keys: + if word in include: + tmp.append(word) + keys = tmp + + items = [] + for k in keys: + items.append((k, self.style[k])) + items = map(lambda i: "%s: %s"%(i[0], i[1]), items) + str = string.join(items, '; ') + ';' + + return str + + + def _escape(self, s): + """ + return a copy of string s with special characters in postscript strings + escaped with backslashes. + Have not handled characters that are converted normally in python strings + i.e. \n -> newline + """ + + str = string.replace(s, chr(0x5C), r'\\' ) + str = string.replace(str, '(', '\(' ) + str = string.replace(str, ')', '\)') + return str + + + def _genArcCode(self, x1, y1, x2, y2, startAng, extent): + """Calculate the path for an arc inscribed in rectangle defined + by (x1,y1),(x2,y2).""" + + return + + #calculate semi-minor and semi-major axes of ellipse + xScale = abs((x2-x1)/2.0) + yScale = abs((y2-y1)/2.0) + #calculate centre of ellipse + x, y = (x1+x2)/2.0, (y1+y2)/2.0 + + codeline = 'matrix currentmatrix %s %s translate %s %s scale 0 0 1 %s %s %s setmatrix' + + if extent >= 0: + arc='arc' + else: + arc='arcn' + data = (x,y, xScale, yScale, startAng, startAng+extent, arc) + + return codeline % data + + + def _fillAndStroke(self, code, clip=0): + path = transformNode(self.doc, "path", + d=self.path, style=self._formatStyle(LINE_STYLES)) + self.currGroup.appendChild(path) + self.path = '' + + return + + """ + if self._fillColor or self._strokeColor or clip: + self.code.extend(code) + if self._fillColor: + if self._strokeColor or clip: + self.code.append("gsave") + self.setColor(self._fillColor) + self.code.append("eofill") + if self._strokeColor or clip: + self.code.append("grestore") + if self._strokeColor != None: + if clip: self.code.append("gsave") + self.setColor(self._strokeColor) + self.code.append("stroke") + if clip: self.code.append("grestore") + if clip: + self.code.append("clip") + self.code.append("newpath") + """ + + + ### styles ### + + def setLineCap(self, v): + vals = {0:'butt', 1:'round', 2:'square'} + if self._lineCap != v: + self._lineCap = v + self.style['stroke-linecap'] = vals[v] + + + def setLineJoin(self, v): + vals = {0:'miter', 1:'round', 2:'bevel'} + if self._lineJoin != v: + self._lineJoin = v + self.style['stroke-linecap'] = vals[v] + + + def setDash(self, array=[], phase=0): + """Two notations. Pass two numbers, or an array and phase.""" + + join = string.join + if type(array) in (types.IntType, types.FloatType): + self.style['stroke-dasharray'] = join(map(str, ([array, phase])), ', ') + elif type(array) in (types.ListType, types.TupleType) and len(array) > 0: + assert phase >= 0, "phase is a length in user space" + self.style['stroke-dasharray'] = join(map(str, (array+[phase])), ', ') + + + def setStrokeColor(self, color): + self._strokeColor = color + self.setColor(color) + if color == None: + self.style['stroke'] = 'none' + else: + r, g, b = color.red, color.green, color.blue + self.style['stroke'] = 'rgb(%d%%,%d%%,%d%%)' % (r*100, g*100, b*100) + + + def setColor(self, color): + if self._color != color: + self._color = color + + + def setFillColor(self, color): + self._fillColor = color + self.setColor(color) + if color == None: + self.style['fill'] = 'none' + else: + r, g, b = color.red, color.green, color.blue + self.style['fill'] = 'rgb(%d%%,%d%%,%d%%)' % (r*100, g*100, b*100) + + + def setLineWidth(self, width): + if width != self._lineWidth: + self._lineWidth = width + self.style['stroke-width'] = width + + + def setFont(self, font, fontSize): + if self._font != font or self._fontSize != fontSize: + self._font, self._fontSize = (font, fontSize) + self.style['font-family'] = font + self.style['font-size'] = fontSize + + + ### shapes ### + + def rect(self, x1,y1, x2,y2, rx=8, ry=8): + "Draw a rectangle between x1,y1 and x2,y2." + + if self.verbose: print "+++ SVGCanvas.rect" + + rect = transformNode(self.doc, "rect", + x=x1, y=y1, width=x2-x1, height=y2-y1, + style=self._formatStyle(LINE_STYLES)) + + self.currGroup.appendChild(rect) + + + def roundRect(self, x1,y1, x2,y2, rx=8, ry=8): + """Draw a rounded rectangle between x1,y1 and x2,y2. + + Corners inset as ellipses with x-radius rx and y-radius ry. + These should have x10, and ry>0. + """ + + rect = transformNode(self.doc, "rect", + x=x1, y=y1, width=x2-x1, height=y2-y1, rx=rx, ry=ry, + style=self._formatStyle(LINE_STYLES)) + + self.currGroup.appendChild(rect) + + + def drawString(self, s, x, y, angle=0): + if self.verbose: print "+++ SVGCanvas.drawString" + + if self._fillColor != None: + self.setColor(self._fillColor) + s = self._escape(s) + st = self._formatStyle(TEXT_STYLES) + if angle != 0: + st = st + " rotate(%f %f %f);" % (angle, x, y) + st = st + " fill: %s;" % self.style['fill'] + text = transformNode(self.doc, "text", + x=x, y=y, style=st, + transform="translate(0,%d) scale(1,-1)" % (2*y)) + content = self.doc.createTextNode(s) + text.appendChild(content) + + self.currGroup.appendChild(text) + + def drawCentredString(self, s, x, y, angle=0,text_anchor='middle'): + if self.verbose: print "+++ SVGCanvas.drawCentredString" + + if self._fillColor != None: + if not text_anchor in ['start', 'inherited']: + textLen = stringWidth(s,self._font,self._fontSize) + if text_anchor=='end': + x -= textLen + elif text_anchor=='middle': + x -= textLen/2. + else: + raise ValueError, 'bad value for text_anchor ' + str(text_anchor) + self.drawString(x,y,text,angle=angle) + + def drawRightString(self, text, x, y, angle=0): + self.drawCentredString(text,x,y,angle=angle,text_anchor='end') + + def comment(self, data): + "Add a comment." + + comment = self.doc.createComment(data) + # self.currGroup.appendChild(comment) + + + def drawImage(self, image, x1, y1, x2=None, y2=None): + pass + + + def line(self, x1, y1, x2, y2): + if self._strokeColor != None: + if 0: # something is wrong with line in my SVG viewer... + line = transformNode(self.doc, "line", + x=x1, y=y1, x2=x2, y2=y2, + style=self._formatStyle(LINE_STYLES)) + self.currGroup.appendChild(line) + path = transformNode(self.doc, "path", + d="M %f,%f L %f,%f Z" % (x1,y1,x2,y2), + style=self._formatStyle(LINE_STYLES)) + self.currGroup.appendChild(path) + + + def ellipse(self, x1, y1, x2, y2): + """Draw an orthogonal ellipse inscribed within the rectangle x1,y1,x2,y2. + + These should have x1=180, 0, mx, my) + else: + str = str + "M %f, %f A %f, %f %d %d %d %f, %f Z " % \ + (mx, my, rx, ry, 0, extent>=180, 0, mx, my) + + if fromcenter: + str = str + "L %f, %f Z " % (cx, cy) + + path = transformNode(self.doc, "path", + d=str, style=self._formatStyle()) + self.currGroup.appendChild(path) + + + def polygon(self, points, closed=0): + assert len(points) >= 2, 'Polygon must have 2 or more points' + + if self._strokeColor != None: + self.setColor(self._strokeColor) + pairs = [] + for i in xrange(len(points)): + pairs.append("%f %f" % (points[i])) + pts = string.join(pairs, ', ') + polyline = transformNode(self.doc, "polygon", + points=pts, style=self._formatStyle(LINE_STYLES)) + self.currGroup.appendChild(polyline) + + # self._fillAndStroke(polyCode) + + + def lines(self, lineList, color=None, width=None): + # print "### lineList", lineList + return + + if self._strokeColor != None: + self._setColor(self._strokeColor) + codeline = '%s m %s l stroke' + for line in lineList: + self.code.append(codeline % (fp_str(line[0]), fp_str(line[1]))) + + + def polyLine(self, points): + assert len(points) >= 1, 'Polyline must have 1 or more points' + + if self._strokeColor != None: + self.setColor(self._strokeColor) + pairs = [] + for i in xrange(len(points)): + pairs.append("%f %f" % (points[i])) + pts = string.join(pairs, ', ') + polyline = transformNode(self.doc, "polyline", + points=pts, style=self._formatStyle(LINE_STYLES)) + self.currGroup.appendChild(polyline) + + + ### groups ### + + def startGroup(self): + if self.verbose: print "+++ begin SVGCanvas.startGroup" + currGroup, group = self.currGroup, transformNode(self.doc, "g", transform="") + currGroup.appendChild(group) + self.currGroup = group + if self.verbose: print "+++ end SVGCanvas.startGroup" + return currGroup + + def endGroup(self,currGroup): + if self.verbose: print "+++ begin SVGCanvas.endGroup" + self.currGroup = currGroup + if self.verbose: print "+++ end SVGCanvas.endGroup" + + + def transform(self, a, b, c, d, e, f): + if self.verbose: print "!!! begin SVGCanvas.transform", a, b, c, d, e, f + tr = self.currGroup.getAttribute("transform") + t = 'matrix(%f, %f, %f, %f, %f, %f)' % (a,b,c,d,e,f) + if (a, b, c, d, e, f) != (1, 0, 0, 1, 0, 0): + self.currGroup.setAttribute("transform", "%s %s" % (tr, t)) + + + def translate(self, x, y): + # probably never used + print "!!! begin SVGCanvas.translate" + return + + tr = self.currGroup.getAttribute("transform") + t = 'translate(%f, %f)' % (x, y) + self.currGroup.setAttribute("transform", "%s %s" % (tr, t)) + + + def scale(self, x, y): + # probably never used + print "!!! begin SVGCanvas.scale" + return + + tr = self.groups[-1].getAttribute("transform") + t = 'scale(%f, %f)' % (x, y) + self.currGroup.setAttribute("transform", "%s %s" % (tr, t)) + + + ### paths ### + + def moveTo(self, x, y): + self.path = self.path + 'M %f %f ' % (x, y) + + + def lineTo(self, x, y): + self.path = self.path + 'L %f %f ' % (x, y) + + + def curveTo(self, x1, y1, x2, y2, x3, y3): + self.path = self.path + 'C %f %f %f %f %f %f ' % (x1, y1, x2, y2, x3, y3) + + + def closePath(self): + self.path = self.path + 'Z ' + + def saveState(self): + pass + + def restoreState(self): + pass + +class _SVGRenderer(Renderer): + """This draws onto an SVG document. + """ + + def __init__(self): + self._tracker = StateTracker() + self.verbose = 0 + + def drawNode(self, node): + """This is the recursive method called for each node in the tree. + """ + + if self.verbose: print "### begin _SVGRenderer.drawNode" + + self._canvas.comment('begin node %s'%`node`) + color = self._canvas._color + if not (isinstance(node, Path) and node.isClipPath): + pass # self._canvas.saveState() + + #apply state changes + deltas = getStateDelta(node) + self._tracker.push(deltas) + self.applyStateChanges(deltas, {}) + + #draw the object, or recurse + self.drawNodeDispatcher(node) + + rDeltas = self._tracker.pop() + if not (isinstance(node, Path) and node.isClipPath): + pass # self._canvas.restoreState() + self._canvas.comment('end node %s'%`node`) + self._canvas._color = color + + #restore things we might have lost (without actually doing anything). + for k, v in rDeltas.items(): + if self._restores.has_key(k): + setattr(self._canvas,self._restores[k],v) + + if self.verbose: print "### end _SVGRenderer.drawNode" + + _restores = {'strokeColor':'_strokeColor','strokeWidth': '_lineWidth','strokeLineCap':'_lineCap', + 'strokeLineJoin':'_lineJoin','fillColor':'_fillColor','fontName':'_font', + 'fontSize':'_fontSize'} + + + def drawGroup(self, group): + if self.verbose: print "### begin _SVGRenderer.drawGroup" + + currGroup = self._canvas.startGroup() + a, b, c, d, e, f = self._tracker.getCTM() + for childNode in group.getContents(): + if isinstance(childNode, UserNode): + node2 = childNode.provideNode() + else: + node2 = childNode + self.drawNode(node2) + self._canvas.transform(a, b, c, d, e, f) + self._canvas.endGroup(currGroup) + + if self.verbose: print "### end _SVGRenderer.drawGroup" + + + def drawRect(self, rect): + if rect.rx == rect.ry == 0: + #plain old rectangle + self._canvas.rect( + rect.x, rect.y, + rect.x+rect.width, rect.y+rect.height) + else: + #cheat and assume ry = rx; better to generalize + #pdfgen roundRect function. TODO + self._canvas.roundRect( + rect.x, rect.y, + rect.x+rect.width, rect.y+rect.height, + rect.rx, rect.ry + ) + + + def drawString(self, stringObj): + if self._canvas._fillColor: + S = self._tracker.getState() + text_anchor, x, y, text = S['textAnchor'], stringObj.x, stringObj.y, stringObj.text + if not text_anchor in ['start', 'inherited']: + font, fontSize = S['fontName'], S['fontSize'] + textLen = stringWidth(text, font,fontSize) + if text_anchor=='end': + x = x-textLen + elif text_anchor=='middle': + x = x - textLen/2 + else: + raise ValueError, 'bad value for text_anchor ' + str(text_anchor) + self._canvas.drawString(text,x,y) + + + def drawLine(self, line): + if self._canvas._strokeColor: + self._canvas.line(line.x1, line.y1, line.x2, line.y2) + + + def drawCircle(self, circle): + self._canvas.circle( circle.cx, circle.cy, circle.r) + + + def drawWedge(self, wedge): + centerx, centery, radius, startangledegrees, endangledegrees = \ + wedge.centerx, wedge.centery, wedge.radius, wedge.startangledegrees, wedge.endangledegrees + yradius = wedge.yradius or wedge.radius + (x1, y1) = (centerx-radius, centery-yradius) + (x2, y2) = (centerx+radius, centery+yradius) + extent = endangledegrees - startangledegrees + self._canvas.drawArc(x1, y1, x2, y2, startangledegrees, extent, fromcenter=1) + + + def drawPolyLine(self, p): + if self._canvas._strokeColor: + self._canvas.polyLine(_pointsFromList(p.points)) + + + def drawEllipse(self, ellipse): + #need to convert to pdfgen's bounding box representation + x1 = ellipse.cx - ellipse.rx + x2 = ellipse.cx + ellipse.rx + y1 = ellipse.cy - ellipse.ry + y2 = ellipse.cy + ellipse.ry + self._canvas.ellipse(x1,y1,x2,y2) + + + def drawPolygon(self, p): + self._canvas.polygon(_pointsFromList(p.points), closed=1) + + + def drawPath(self, path): + # print "### drawPath", path.points + from reportlab.graphics.shapes import _renderPath + c = self._canvas + drawFuncs = (c.moveTo, c.lineTo, c.curveTo, c.closePath) + isClosed = _renderPath(path, drawFuncs) + if not isClosed: + c._fillColor = None + c._fillAndStroke([], clip=path.isClipPath) + + + def applyStateChanges(self, delta, newState): + """This takes a set of states, and outputs the operators + needed to set those properties""" + + for key, value in delta.items(): + if key == 'transform': + pass + #self._canvas.transform(value[0], value[1], value[2], value[3], value[4], value[5]) + elif key == 'strokeColor': + self._canvas.setStrokeColor(value) + elif key == 'strokeWidth': + self._canvas.setLineWidth(value) + elif key == 'strokeLineCap': #0,1,2 + self._canvas.setLineCap(value) + elif key == 'strokeLineJoin': + self._canvas.setLineJoin(value) + elif key == 'strokeDashArray': + if value: + self._canvas.setDash(value) + else: + self._canvas.setDash() + elif key == 'fillColor': + self._canvas.setFillColor(value) + elif key in ['fontSize', 'fontName']: + fontname = delta.get('fontName', self._canvas._font) + fontsize = delta.get('fontSize', self._canvas._fontSize) + self._canvas.setFont(fontname, fontsize) + + + + +def test0(outdir='svgout'): + # print all drawings and their doc strings from the test + # file + if not os.path.isdir(outdir): + os.mkdir(outdir) + #grab all drawings from the test module + from reportlab.graphics import testshapes + drawings = [] + + for funcname in dir(testshapes): + #if funcname[0:11] == 'getDrawing2': + # print 'hacked to only show drawing 2' + if funcname[0:10] == 'getDrawing': + drawing = eval('testshapes.' + funcname + '()') + docstring = eval('testshapes.' + funcname + '.__doc__') + drawings.append((drawing, docstring)) + + # return + + i = 0 + for (d, docstring) in drawings: + filename = outdir + os.sep + 'renderSVG_%d.svg' % i + drawToFile(d, filename) + # print 'saved', filename + i = i + 1 + + +def test1(): + from reportlab.graphics.testshapes import getDrawing01 + d = getDrawing01() + drawToFile(d, "svgout/test.svg") + + +def test2(): + from reportlab.lib.corp import RL_CorpLogo + from reportlab.graphics.shapes import Drawing + + rl = RL_CorpLogo() + d = Drawing(rl.width,rl.height) + d.add(rl) + drawToFile(d, "svgout/corplogo.svg") + + +if __name__=='__main__': + test0() + test1() + test2() diff --git a/bin/reportlab/graphics/renderbase.py b/bin/reportlab/graphics/renderbase.py new file mode 100644 index 00000000000..10282e15af4 --- /dev/null +++ b/bin/reportlab/graphics/renderbase.py @@ -0,0 +1,351 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/renderbase.py +""" +Superclass for renderers to factor out common functionality and default implementations. +""" + + +__version__=''' $Id $ ''' + +from reportlab.graphics.shapes import * +from reportlab.lib.validators import DerivedValue +from reportlab import rl_config + +def inverse(A): + "For A affine 2D represented as 6vec return 6vec version of A**(-1)" + # I checked this RGB + det = float(A[0]*A[3] - A[2]*A[1]) + R = [A[3]/det, -A[1]/det, -A[2]/det, A[0]/det] + return tuple(R+[-R[0]*A[4]-R[2]*A[5],-R[1]*A[4]-R[3]*A[5]]) + +def mmult(A, B): + "A postmultiplied by B" + # I checked this RGB + # [a0 a2 a4] [b0 b2 b4] + # [a1 a3 a5] * [b1 b3 b5] + # [ 1 ] [ 1 ] + # + return (A[0]*B[0] + A[2]*B[1], + A[1]*B[0] + A[3]*B[1], + A[0]*B[2] + A[2]*B[3], + A[1]*B[2] + A[3]*B[3], + A[0]*B[4] + A[2]*B[5] + A[4], + A[1]*B[4] + A[3]*B[5] + A[5]) + + +def getStateDelta(shape): + """Used to compute when we need to change the graphics state. + For example, if we have two adjacent red shapes we don't need + to set the pen color to red in between. Returns the effect + the given shape would have on the graphics state""" + delta = {} + for (prop, value) in shape.getProperties().items(): + if STATE_DEFAULTS.has_key(prop): + delta[prop] = value + return delta + + +class StateTracker: + """Keeps a stack of transforms and state + properties. It can contain any properties you + want, but the keys 'transform' and 'ctm' have + special meanings. The getCTM() + method returns the current transformation + matrix at any point, without needing to + invert matrixes when you pop.""" + def __init__(self, defaults=None): + # one stack to keep track of what changes... + self._deltas = [] + + # and another to keep track of cumulative effects. Last one in + # list is the current graphics state. We put one in to simplify + # loops below. + self._combined = [] + if defaults is None: + defaults = STATE_DEFAULTS.copy() + #ensure that if we have a transform, we have a CTM + if defaults.has_key('transform'): + defaults['ctm'] = defaults['transform'] + self._combined.append(defaults) + + def push(self,delta): + """Take a new state dictionary of changes and push it onto + the stack. After doing this, the combined state is accessible + through getState()""" + + newstate = self._combined[-1].copy() + for (key, value) in delta.items(): + if key == 'transform': #do cumulative matrix + newstate['transform'] = delta['transform'] + newstate['ctm'] = mmult(self._combined[-1]['ctm'], delta['transform']) + #print 'statetracker transform = (%0.2f, %0.2f, %0.2f, %0.2f, %0.2f, %0.2f)' % tuple(newstate['transform']) + #print 'statetracker ctm = (%0.2f, %0.2f, %0.2f, %0.2f, %0.2f, %0.2f)' % tuple(newstate['ctm']) + + else: #just overwrite it + newstate[key] = value + + self._combined.append(newstate) + self._deltas.append(delta) + + def pop(self): + """steps back one, and returns a state dictionary with the + deltas to reverse out of wherever you are. Depending + on your back end, you may not need the return value, + since you can get the complete state afterwards with getState()""" + del self._combined[-1] + newState = self._combined[-1] + lastDelta = self._deltas[-1] + del self._deltas[-1] + #need to diff this against the last one in the state + reverseDelta = {} + #print 'pop()...' + for key, curValue in lastDelta.items(): + #print ' key=%s, value=%s' % (key, curValue) + prevValue = newState[key] + if prevValue <> curValue: + #print ' state popping "%s"="%s"' % (key, curValue) + if key == 'transform': + reverseDelta[key] = inverse(lastDelta['transform']) + else: #just return to previous state + reverseDelta[key] = prevValue + return reverseDelta + + def getState(self): + "returns the complete graphics state at this point" + return self._combined[-1] + + def getCTM(self): + "returns the current transformation matrix at this point""" + return self._combined[-1]['ctm'] + + def __getitem__(self,key): + "returns the complete graphics state value of key at this point" + return self._combined[-1][key] + + def __setitem__(self,key,value): + "sets the complete graphics state value of key to value" + self._combined[-1][key] = value + +def testStateTracker(): + print 'Testing state tracker' + defaults = {'fillColor':None, 'strokeColor':None,'fontName':None, 'transform':[1,0,0,1,0,0]} + deltas = [ + {'fillColor':'red'}, + {'fillColor':'green', 'strokeColor':'blue','fontName':'Times-Roman'}, + {'transform':[0.5,0,0,0.5,0,0]}, + {'transform':[0.5,0,0,0.5,2,3]}, + {'strokeColor':'red'} + ] + + st = StateTracker(defaults) + print 'initial:', st.getState() + print + for delta in deltas: + print 'pushing:', delta + st.push(delta) + print 'state: ',st.getState(),'\n' + + for delta in deltas: + print 'popping:',st.pop() + print 'state: ',st.getState(),'\n' + + +def _expandUserNode(node,canvas): + if isinstance(node, UserNode): + try: + if hasattr(node,'_canvas'): + ocanvas = 1 + else: + node._canvas = canvas + ocanvas = None + onode = node + node = node.provideNode() + finally: + if not ocanvas: del onode._canvas + return node + +def renderScaledDrawing(d): + renderScale = d.renderScale + if renderScale!=1.0: + d = d.copy() + d.width *= renderScale + d.height *= renderScale + d.scale(renderScale,renderScale) + d.renderScale = 1.0 + return d + +class Renderer: + """Virtual superclass for graphics renderers.""" + + def __init__(self): + self._tracker = StateTracker() + self._nodeStack = [] #track nodes visited + + def undefined(self, operation): + raise ValueError, "%s operation not defined at superclass class=%s" %(operation, self.__class__) + + def draw(self, drawing, canvas, x=0, y=0, showBoundary=rl_config._unset_): + """This is the top level function, which draws the drawing at the given + location. The recursive part is handled by drawNode.""" + #stash references for ease of communication + if showBoundary is rl_config._unset_: showBoundary=rl_config.showBoundary + self._canvas = canvas + canvas.__dict__['_drawing'] = self._drawing = drawing + drawing._parent = None + try: + #bounding box + if showBoundary: canvas.rect(x, y, drawing.width, drawing.height) + canvas.saveState() + self.initState(x,y) #this is the push() + self.drawNode(drawing) + self.pop() + canvas.restoreState() + finally: + #remove any circular references + del self._canvas, self._drawing, canvas._drawing, drawing._parent + + def initState(self,x,y): + deltas = STATE_DEFAULTS.copy() + deltas['transform'] = [1,0,0,1,x,y] + self._tracker.push(deltas) + self.applyStateChanges(deltas, {}) + + def pop(self): + self._tracker.pop() + + def drawNode(self, node): + """This is the recursive method called for each node + in the tree""" + # Undefined here, but with closer analysis probably can be handled in superclass + self.undefined("drawNode") + + def getStateValue(self, key): + """Return current state parameter for given key""" + currentState = self._tracker._combined[-1] + return currentState[key] + + def fillDerivedValues(self, node): + """Examine a node for any values which are Derived, + and replace them with their calculated values. + Generally things may look at the drawing or their + parent. + + """ + for (key, value) in node.__dict__.items(): + if isinstance(value, DerivedValue): + #just replace with default for key? + #print ' fillDerivedValues(%s)' % key + newValue = value.getValue(self, key) + #print ' got value of %s' % newValue + node.__dict__[key] = newValue + + def drawNodeDispatcher(self, node): + """dispatch on the node's (super) class: shared code""" + + canvas = getattr(self,'_canvas',None) + # replace UserNode with its contents + + try: + node = _expandUserNode(node,canvas) + if hasattr(node,'_canvas'): + ocanvas = 1 + else: + node._canvas = canvas + ocanvas = None + + self.fillDerivedValues(node) + #draw the object, or recurse + if isinstance(node, Line): + self.drawLine(node) + elif isinstance(node, Image): + self.drawImage(node) + elif isinstance(node, Rect): + self.drawRect(node) + elif isinstance(node, Circle): + self.drawCircle(node) + elif isinstance(node, Ellipse): + self.drawEllipse(node) + elif isinstance(node, PolyLine): + self.drawPolyLine(node) + elif isinstance(node, Polygon): + self.drawPolygon(node) + elif isinstance(node, Path): + self.drawPath(node) + elif isinstance(node, String): + self.drawString(node) + elif isinstance(node, Group): + self.drawGroup(node) + elif isinstance(node, Wedge): + self.drawWedge(node) + else: + print 'DrawingError','Unexpected element %s in drawing!' % str(node) + finally: + if not ocanvas: del node._canvas + + _restores = {'stroke':'_stroke','stroke_width': '_lineWidth','stroke_linecap':'_lineCap', + 'stroke_linejoin':'_lineJoin','fill':'_fill','font_family':'_font', + 'font_size':'_fontSize'} + + def drawGroup(self, group): + # just do the contents. Some renderers might need to override this + # if they need a flipped transform + canvas = getattr(self,'_canvas',None) + for node in group.getContents(): + node = _expandUserNode(node,canvas) + + #here is where we do derived values - this seems to get everything. Touch wood. + self.fillDerivedValues(node) + try: + if hasattr(node,'_canvas'): + ocanvas = 1 + else: + node._canvas = canvas + ocanvas = None + node._parent = group + self.drawNode(node) + finally: + del node._parent + if not ocanvas: del node._canvas + + def drawWedge(self, wedge): + # by default ask the wedge to make a polygon of itself and draw that! + #print "drawWedge" + polygon = wedge.asPolygon() + self.drawPolygon(polygon) + + def drawPath(self, path): + polygons = path.asPolygons() + for polygon in polygons: + self.drawPolygon(polygon) + + def drawRect(self, rect): + # could be implemented in terms of polygon + self.undefined("drawRect") + + def drawLine(self, line): + self.undefined("drawLine") + + def drawCircle(self, circle): + self.undefined("drawCircle") + + def drawPolyLine(self, p): + self.undefined("drawPolyLine") + + def drawEllipse(self, ellipse): + self.undefined("drawEllipse") + + def drawPolygon(self, p): + self.undefined("drawPolygon") + + def drawString(self, stringObj): + self.undefined("drawString") + + def applyStateChanges(self, delta, newState): + """This takes a set of states, and outputs the operators + needed to set those properties""" + self.undefined("applyStateChanges") + +if __name__=='__main__': + print "this file has no script interpretation" + print __doc__ diff --git a/bin/reportlab/graphics/samples/__init__.py b/bin/reportlab/graphics/samples/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/bin/reportlab/graphics/samples/bubble.py b/bin/reportlab/graphics/samples/bubble.py new file mode 100644 index 00000000000..64bf3137912 --- /dev/null +++ b/bin/reportlab/graphics/samples/bubble.py @@ -0,0 +1,73 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.charts.lineplots import ScatterPlot +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class Bubble(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,ScatterPlot(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.lines[0].strokeColor = color01 + self.chart.lines[1].strokeColor = color02 + self.chart.lines[2].strokeColor = color03 + self.chart.lines[3].strokeColor = color04 + self.chart.lines[4].strokeColor = color05 + self.chart.lines[5].strokeColor = color06 + self.chart.lines[6].strokeColor = color07 + self.chart.lines[7].strokeColor = color08 + self.chart.lines[8].strokeColor = color09 + self.chart.lines[9].strokeColor = color10 + self.chart.lines.symbol.kind ='Circle' + self.chart.lines.symbol.size = 15 + self.chart.fillColor = backgroundGrey + self.chart.lineLabels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontSize = 7 + self.chart.xValueAxis.forceZero = 0 + self.chart.data = [((100,100), (200,200), (250,210), (300,300), (350,450))] + self.chart.xValueAxis.avoidBoundFrac = 1 + self.chart.xValueAxis.gridEnd = 115 + self.chart.xValueAxis.tickDown = 3 + self.chart.xValueAxis.visibleGrid = 1 + self.chart.yValueAxis.tickLeft = 3 + self.chart.yValueAxis.labels.fontName = 'Helvetica' + self.chart.yValueAxis.labels.fontSize = 7 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self.chart.lineLabelFormat = None + self.chart.xLabel = 'X Axis' + self.chart.y = 30 + self.chart.yLabel = 'Y Axis' + self.chart.yValueAxis.labelTextFormat = '%d' + self.chart.yValueAxis.forceZero = 1 + self.chart.xValueAxis.forceZero = 1 + + + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + Bubble().save(formats=['pdf'],outDir=None,fnRoot='bubble') diff --git a/bin/reportlab/graphics/samples/clustered_bar.py b/bin/reportlab/graphics/samples/clustered_bar.py new file mode 100644 index 00000000000..4d8c363eb29 --- /dev/null +++ b/bin/reportlab/graphics/samples/clustered_bar.py @@ -0,0 +1,84 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from excelcolors import * +from reportlab.graphics.charts.barcharts import HorizontalBarChart +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label + +class ClusteredBar(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,HorizontalBarChart(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.bars[0].fillColor = color01 + self.chart.bars[1].fillColor = color02 + self.chart.bars[2].fillColor = color03 + self.chart.bars[3].fillColor = color04 + self.chart.bars[4].fillColor = color05 + self.chart.bars[5].fillColor = color06 + self.chart.bars[6].fillColor = color07 + self.chart.bars[7].fillColor = color08 + self.chart.bars[8].fillColor = color09 + self.chart.bars[9].fillColor = color10 + self.chart.fillColor = backgroundGrey + self.chart.barLabels.fontName = 'Helvetica' + self.chart.valueAxis.labels.fontName = 'Helvetica' + self.chart.valueAxis.labels.fontSize = 6 + self.chart.valueAxis.forceZero = 1 + self.chart.data = [(100, 150, 180), (125, 180, 200)] + self.chart.groupSpacing = 15 + self.chart.valueAxis.avoidBoundFrac = 1 + self.chart.valueAxis.gridEnd = 80 + self.chart.valueAxis.tickDown = 3 + self.chart.valueAxis.visibleGrid = 1 + self.chart.categoryAxis.categoryNames = ['North', 'South', 'Central'] + self.chart.categoryAxis.tickLeft = 3 + self.chart.categoryAxis.labels.fontName = 'Helvetica' + self.chart.categoryAxis.labels.fontSize = 6 + self.chart.categoryAxis.labels.dx = -3 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self._add(self,Label(),name='XLabel',validate=None,desc="The label on the horizontal axis") + self.XLabel.fontName = 'Helvetica' + self.XLabel.fontSize = 7 + self.XLabel.x = 85 + self.XLabel.y = 10 + self.XLabel.textAnchor ='middle' + self.XLabel.maxWidth = 100 + self.XLabel.height = 20 + self.XLabel._text = "X Axis" + self._add(self,Label(),name='YLabel',validate=None,desc="The label on the vertical axis") + self.YLabel.fontName = 'Helvetica' + self.YLabel.fontSize = 7 + self.YLabel.x = 12 + self.YLabel.y = 80 + self.YLabel.angle = 90 + self.YLabel.textAnchor ='middle' + self.YLabel.maxWidth = 100 + self.YLabel.height = 20 + self.YLabel._text = "Y Axis" + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + ClusteredBar().save(formats=['pdf'],outDir=None,fnRoot='clustered_bar') \ No newline at end of file diff --git a/bin/reportlab/graphics/samples/clustered_column.py b/bin/reportlab/graphics/samples/clustered_column.py new file mode 100644 index 00000000000..8ea9542eadc --- /dev/null +++ b/bin/reportlab/graphics/samples/clustered_column.py @@ -0,0 +1,83 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from excelcolors import * +from reportlab.graphics.charts.barcharts import VerticalBarChart +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label + +class ClusteredColumn(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,VerticalBarChart(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.bars[0].fillColor = color01 + self.chart.bars[1].fillColor = color02 + self.chart.bars[2].fillColor = color03 + self.chart.bars[3].fillColor = color04 + self.chart.bars[4].fillColor = color05 + self.chart.bars[5].fillColor = color06 + self.chart.bars[6].fillColor = color07 + self.chart.bars[7].fillColor = color08 + self.chart.bars[8].fillColor = color09 + self.chart.bars[9].fillColor = color10 + self.chart.fillColor = backgroundGrey + self.chart.barLabels.fontName = 'Helvetica' + self.chart.valueAxis.labels.fontName = 'Helvetica' + self.chart.valueAxis.labels.fontSize = 7 + self.chart.valueAxis.forceZero = 1 + self.chart.data = [(100, 150, 180), (125, 180, 200)] + self.chart.groupSpacing = 15 + self.chart.valueAxis.avoidBoundFrac = 1 + self.chart.valueAxis.gridEnd = 115 + self.chart.valueAxis.tickLeft = 3 + self.chart.valueAxis.visibleGrid = 1 + self.chart.categoryAxis.categoryNames = ['North', 'South', 'Central'] + self.chart.categoryAxis.tickDown = 3 + self.chart.categoryAxis.labels.fontName = 'Helvetica' + self.chart.categoryAxis.labels.fontSize = 7 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self._add(self,Label(),name='XLabel',validate=None,desc="The label on the horizontal axis") + self.XLabel.fontName = 'Helvetica' + self.XLabel.fontSize = 7 + self.XLabel.x = 85 + self.XLabel.y = 10 + self.XLabel.textAnchor ='middle' + self.XLabel.maxWidth = 100 + self.XLabel.height = 20 + self.XLabel._text = "X Axis" + self._add(self,Label(),name='YLabel',validate=None,desc="The label on the vertical axis") + self.YLabel.fontName = 'Helvetica' + self.YLabel.fontSize = 7 + self.YLabel.x = 12 + self.YLabel.y = 80 + self.YLabel.angle = 90 + self.YLabel.textAnchor ='middle' + self.YLabel.maxWidth = 100 + self.YLabel.height = 20 + self.YLabel._text = "Y Axis" + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + ClusteredColumn().save(formats=['pdf'],outDir=None,fnRoot='clustered_column') diff --git a/bin/reportlab/graphics/samples/excelcolors.py b/bin/reportlab/graphics/samples/excelcolors.py new file mode 100644 index 00000000000..f3f4f5b6950 --- /dev/null +++ b/bin/reportlab/graphics/samples/excelcolors.py @@ -0,0 +1,45 @@ +# define standard colors to mimic those used by Microsoft Excel +from reportlab.lib.colors import CMYKColor, PCMYKColor + +#colour names as comments at the end of each line are as a memory jogger ONLY +#NOT HTML named colours! + +#Main colours as used for bars etc +color01 = PCMYKColor(40,40,0,0) # Lavender +color02 = PCMYKColor(0,66,33,39) # Maroon +color03 = PCMYKColor(0,0,20,0) # Yellow +color04 = PCMYKColor(20,0,0,0) # Cyan +color05 = PCMYKColor(0,100,0,59) # Purple +color06 = PCMYKColor(0,49,49,0) # Salmon +color07 = PCMYKColor(100,49,0,19) # Blue +color08 = PCMYKColor(20,20,0,0) # PaleLavender +color09 = PCMYKColor(100,100,0,49) # NavyBlue +color10 = PCMYKColor(0,100,0,0) # Purple + +#Highlight colors - eg for the tops of bars +color01Light = PCMYKColor(39,39,0,25) # Light Lavender +color02Light = PCMYKColor(0,66,33,54) # Light Maroon +color03Light = PCMYKColor(0,0,19,25) # Light Yellow +color04Light = PCMYKColor(19,0,0,25) # Light Cyan +color05Light = PCMYKColor(0,100,0,69) # Light Purple +color06Light = PCMYKColor(0,49,49,25) # Light Salmon +color07Light = PCMYKColor(100,49,0,39) # Light Blue +color08Light = PCMYKColor(19,19,0,25) # Light PaleLavender +color09Light = PCMYKColor(100,100,0,62) # Light NavyBlue +color10Light = PCMYKColor(0,100,0,25) # Light Purple + +#Lowlight colors - eg for the sides of bars +color01Dark = PCMYKColor(39,39,0,49) # Dark Lavender +color02Dark = PCMYKColor(0,66,33,69) # Dark Maroon +color03Dark = PCMYKColor(0,0,20,49) # Dark Yellow +color04Dark = PCMYKColor(20,0,0,49) # Dark Cyan +color05Dark = PCMYKColor(0,100,0,80) # Dark Purple +color06Dark = PCMYKColor(0,50,50,49) # Dark Salmon +color07Dark = PCMYKColor(100,50,0,59) # Dark Blue +color08Dark = PCMYKColor(20,20,0,49) # Dark PaleLavender +color09Dark = PCMYKColor(100,100,0,79) # Dark NavyBlue +color10Dark = PCMYKColor(0,100,0,49) # Dark Purple + +#for standard grey backgrounds +backgroundGrey = PCMYKColor(0,0,0,24) + diff --git a/bin/reportlab/graphics/samples/exploded_pie.py b/bin/reportlab/graphics/samples/exploded_pie.py new file mode 100644 index 00000000000..8076493bd82 --- /dev/null +++ b/bin/reportlab/graphics/samples/exploded_pie.py @@ -0,0 +1,65 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.piecharts import Pie +from excelcolors import * +from reportlab.graphics.widgets.grids import ShadedRect +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label + +class ExplodedPie(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,Pie(),name='chart',validate=None,desc="The main chart") + self.chart.width = 100 + self.chart.height = 100 + self.chart.x = 25 + self.chart.y = 25 + self.chart.slices[0].fillColor = color01 + self.chart.slices[1].fillColor = color02 + self.chart.slices[2].fillColor = color03 + self.chart.slices[3].fillColor = color04 + self.chart.slices[4].fillColor = color05 + self.chart.slices[5].fillColor = color06 + self.chart.slices[6].fillColor = color07 + self.chart.slices[7].fillColor = color08 + self.chart.slices[8].fillColor = color09 + self.chart.slices[9].fillColor = color10 + self.chart.data = (100, 150, 180) + self.chart.startAngle = -90 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'North'), (color02, 'South'), (color03, 'Central')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 160 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self.Legend.columnMaximum = 10 + self.chart.slices.strokeWidth = 1 + self.chart.slices.fontName = 'Helvetica' + self.background = ShadedRect() + self.background.fillColorStart = backgroundGrey + self.background.fillColorEnd = backgroundGrey + self.background.numShades = 1 + self.background.strokeWidth = 0.5 + self.background.x = 20 + self.background.y = 20 + self.chart.slices.popout = 5 + self.background.height = 110 + self.background.width = 110 + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + ExplodedPie().save(formats=['pdf'],outDir=None,fnRoot='exploded_pie') \ No newline at end of file diff --git a/bin/reportlab/graphics/samples/filled_radar.py b/bin/reportlab/graphics/samples/filled_radar.py new file mode 100644 index 00000000000..8439aa235c2 --- /dev/null +++ b/bin/reportlab/graphics/samples/filled_radar.py @@ -0,0 +1,54 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.charts.spider import SpiderChart +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class FilledRadarChart(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,SpiderChart(),name='chart',validate=None,desc="The main chart") + self.chart.width = 90 + self.chart.height = 90 + self.chart.x = 45 + self.chart.y = 25 + self.chart.strands[0].fillColor = color01 + self.chart.strands[1].fillColor = color02 + self.chart.strands[2].fillColor = color03 + self.chart.strands[3].fillColor = color04 + self.chart.strands[4].fillColor = color05 + self.chart.strands[5].fillColor = color06 + self.chart.strands[6].fillColor = color07 + self.chart.strands[7].fillColor = color08 + self.chart.strands[8].fillColor = color09 + self.chart.strands[9].fillColor = color10 + self.chart.strandLabels.fontName = 'Helvetica' + self.chart.strandLabels.fontSize = 6 + self.chart.fillColor = backgroundGrey + self.chart.data = [(125, 180, 200), (100, 150, 180)] + self.chart.labels = ['North', 'South', 'Central'] + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + FilledRadarChart().save(formats=['pdf'],outDir=None,fnRoot='filled_radar') diff --git a/bin/reportlab/graphics/samples/line_chart.py b/bin/reportlab/graphics/samples/line_chart.py new file mode 100644 index 00000000000..49563a94766 --- /dev/null +++ b/bin/reportlab/graphics/samples/line_chart.py @@ -0,0 +1,83 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.charts.lineplots import LinePlot +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class LineChart(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,LinePlot(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.lines[0].strokeColor = color01 + self.chart.lines[1].strokeColor = color02 + self.chart.lines[2].strokeColor = color03 + self.chart.lines[3].strokeColor = color04 + self.chart.lines[4].strokeColor = color05 + self.chart.lines[5].strokeColor = color06 + self.chart.lines[6].strokeColor = color07 + self.chart.lines[7].strokeColor = color08 + self.chart.lines[8].strokeColor = color09 + self.chart.lines[9].strokeColor = color10 + self.chart.fillColor = backgroundGrey + self.chart.lineLabels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontSize = 7 + self.chart.xValueAxis.forceZero = 0 + self.chart.data = [((0, 50), (100,100), (200,200), (250,210), (300,300), (400,500)), ((0, 150), (100,200), (200,300), (250,200), (300,400), (400, 600))] + self.chart.xValueAxis.avoidBoundFrac = 1 + self.chart.xValueAxis.gridEnd = 115 + self.chart.xValueAxis.tickDown = 3 + self.chart.xValueAxis.visibleGrid = 1 + self.chart.yValueAxis.tickLeft = 3 + self.chart.yValueAxis.labels.fontName = 'Helvetica' + self.chart.yValueAxis.labels.fontSize = 7 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self._add(self,Label(),name='XLabel',validate=None,desc="The label on the horizontal axis") + self.XLabel.fontName = 'Helvetica' + self.XLabel.fontSize = 7 + self.XLabel.x = 85 + self.XLabel.y = 10 + self.XLabel.textAnchor ='middle' + self.XLabel.maxWidth = 100 + self.XLabel.height = 20 + self.XLabel._text = "X Axis" + self._add(self,Label(),name='YLabel',validate=None,desc="The label on the vertical axis") + self.YLabel.fontName = 'Helvetica' + self.YLabel.fontSize = 7 + self.YLabel.x = 12 + self.YLabel.y = 80 + self.YLabel.angle = 90 + self.YLabel.textAnchor ='middle' + self.YLabel.maxWidth = 100 + self.YLabel.height = 20 + self.YLabel._text = "Y Axis" + self.chart.yValueAxis.forceZero = 1 + self.chart.xValueAxis.forceZero = 1 + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + LineChart().save(formats=['pdf'],outDir=None,fnRoot='line_chart') diff --git a/bin/reportlab/graphics/samples/linechart_with_markers.py b/bin/reportlab/graphics/samples/linechart_with_markers.py new file mode 100644 index 00000000000..2875cecb5d7 --- /dev/null +++ b/bin/reportlab/graphics/samples/linechart_with_markers.py @@ -0,0 +1,94 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.charts.lineplots import LinePlot +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.widgets.markers import makeMarker +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class LineChartWithMarkers(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,LinePlot(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.lines[0].strokeColor = color01 + self.chart.lines[1].strokeColor = color02 + self.chart.lines[2].strokeColor = color03 + self.chart.lines[3].strokeColor = color04 + self.chart.lines[4].strokeColor = color05 + self.chart.lines[5].strokeColor = color06 + self.chart.lines[6].strokeColor = color07 + self.chart.lines[7].strokeColor = color08 + self.chart.lines[8].strokeColor = color09 + self.chart.lines[9].strokeColor = color10 + self.chart.lines[0].symbol = makeMarker('FilledSquare') + self.chart.lines[1].symbol = makeMarker('FilledDiamond') + self.chart.lines[2].symbol = makeMarker('FilledStarFive') + self.chart.lines[3].symbol = makeMarker('FilledTriangle') + self.chart.lines[4].symbol = makeMarker('FilledCircle') + self.chart.lines[5].symbol = makeMarker('FilledPentagon') + self.chart.lines[6].symbol = makeMarker('FilledStarSix') + self.chart.lines[7].symbol = makeMarker('FilledHeptagon') + self.chart.lines[8].symbol = makeMarker('FilledOctagon') + self.chart.lines[9].symbol = makeMarker('FilledCross') + self.chart.fillColor = backgroundGrey + self.chart.lineLabels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontSize = 7 + self.chart.xValueAxis.forceZero = 0 + self.chart.data = [((0, 50), (100,100), (200,200), (250,210), (300,300), (400,500)), ((0, 150), (100,200), (200,300), (250,200), (300,400), (400, 600))] + self.chart.xValueAxis.avoidBoundFrac = 1 + self.chart.xValueAxis.gridEnd = 115 + self.chart.xValueAxis.tickDown = 3 + self.chart.xValueAxis.visibleGrid = 1 + self.chart.yValueAxis.tickLeft = 3 + self.chart.yValueAxis.labels.fontName = 'Helvetica' + self.chart.yValueAxis.labels.fontSize = 7 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self._add(self,Label(),name='XLabel',validate=None,desc="The label on the horizontal axis") + self.XLabel.fontName = 'Helvetica' + self.XLabel.fontSize = 7 + self.XLabel.x = 85 + self.XLabel.y = 10 + self.XLabel.textAnchor ='middle' + self.XLabel.maxWidth = 100 + self.XLabel.height = 20 + self.XLabel._text = "X Axis" + self._add(self,Label(),name='YLabel',validate=None,desc="The label on the vertical axis") + self.YLabel.fontName = 'Helvetica' + self.YLabel.fontSize = 7 + self.YLabel.x = 12 + self.YLabel.y = 80 + self.YLabel.angle = 90 + self.YLabel.textAnchor ='middle' + self.YLabel.maxWidth = 100 + self.YLabel.height = 20 + self.YLabel._text = "Y Axis" + self.chart.yValueAxis.forceZero = 1 + self.chart.xValueAxis.forceZero = 1 + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + LineChartWithMarkers().save(formats=['pdf'],outDir=None,fnRoot='linechart_with_markers') diff --git a/bin/reportlab/graphics/samples/radar.py b/bin/reportlab/graphics/samples/radar.py new file mode 100644 index 00000000000..6d4d8d7b8e7 --- /dev/null +++ b/bin/reportlab/graphics/samples/radar.py @@ -0,0 +1,66 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from excelcolors import * +from reportlab.graphics.charts.spider import SpiderChart +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label + +class RadarChart(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,SpiderChart(),name='chart',validate=None,desc="The main chart") + self.chart.width = 90 + self.chart.height = 90 + self.chart.x = 45 + self.chart.y = 25 + self.chart.strands[0].strokeColor= color01 + self.chart.strands[1].strokeColor= color02 + self.chart.strands[2].strokeColor= color03 + self.chart.strands[3].strokeColor= color04 + self.chart.strands[4].strokeColor= color05 + self.chart.strands[5].strokeColor= color06 + self.chart.strands[6].strokeColor= color07 + self.chart.strands[7].strokeColor= color08 + self.chart.strands[8].strokeColor= color09 + self.chart.strands[9].strokeColor= color10 + self.chart.strands[0].fillColor = None + self.chart.strands[1].fillColor = None + self.chart.strands[2].fillColor = None + self.chart.strands[3].fillColor = None + self.chart.strands[4].fillColor = None + self.chart.strands[5].fillColor = None + self.chart.strands[6].fillColor = None + self.chart.strands[7].fillColor = None + self.chart.strands[8].fillColor = None + self.chart.strands[9].fillColor = None + self.chart.strands.strokeWidth = 1 + self.chart.strandLabels.fontName = 'Helvetica' + self.chart.strandLabels.fontSize = 6 + self.chart.fillColor = backgroundGrey + self.chart.data = [(125, 180, 200), (100, 150, 180)] + self.chart.labels = ['North', 'South', 'Central'] + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self.chart.strands.strokeWidth = 1 + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + RadarChart().save(formats=['pdf'],outDir=None,fnRoot='radar') diff --git a/bin/reportlab/graphics/samples/runall.py b/bin/reportlab/graphics/samples/runall.py new file mode 100644 index 00000000000..938ab8fec42 --- /dev/null +++ b/bin/reportlab/graphics/samples/runall.py @@ -0,0 +1,59 @@ +# runs all the GUIedit charts in this directory - +# makes a PDF sample for eaxh existing chart type +import sys +import glob +import string +import inspect +import types + +def moduleClasses(mod): + def P(obj, m=mod.__name__, CT=types.ClassType): + return (type(obj)==CT and obj.__module__==m) + try: + return inspect.getmembers(mod, P)[0][1] + except: + return None + +def getclass(f): + return moduleClasses(__import__(f)) + +def run(format, VERBOSE=0): + formats = string.split(format, ',') + for i in range(0, len(formats)): + formats[i] == string.lower(string.strip(formats[i])) + allfiles = glob.glob('*.py') + allfiles.sort() + for fn in allfiles: + f = string.split(fn, '.')[0] + c = getclass(f) + if c != None: + print c.__name__ + try: + for fmt in formats: + if fmt: + c().save(formats=[fmt],outDir='.',fnRoot=c.__name__) + if VERBOSE: + print " %s.%s" % (c.__name__, fmt) + except: + print " COULDN'T CREATE '%s.%s'!" % (c.__name__, format) + +if __name__ == "__main__": + if len(sys.argv) == 1: + run('pdf,pict,png') + else: + try: + if sys.argv[1] == "-h": + print 'usage: runall.py [FORMAT] [-h]' + print ' if format is supplied is should be one or more of pdf,gif,eps,png etc' + print ' if format is missing the following formats are assumed: pdf,pict,png' + print ' -h prints this message' + else: + t = sys.argv[1:] + for f in t: + run(f) + except: + print 'usage: runall.py [FORMAT][-h]' + print ' if format is supplied is should be one or more of pdf,gif,eps,png etc' + print ' if format is missing the following formats are assumed: pdf,pict,png' + print ' -h prints this message' + raise diff --git a/bin/reportlab/graphics/samples/scatter.py b/bin/reportlab/graphics/samples/scatter.py new file mode 100644 index 00000000000..ea1a9991d3e --- /dev/null +++ b/bin/reportlab/graphics/samples/scatter.py @@ -0,0 +1,71 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.charts.lineplots import ScatterPlot +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class Scatter(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,ScatterPlot(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.lines[0].strokeColor = color01 + self.chart.lines[1].strokeColor = color02 + self.chart.lines[2].strokeColor = color03 + self.chart.lines[3].strokeColor = color04 + self.chart.lines[4].strokeColor = color05 + self.chart.lines[5].strokeColor = color06 + self.chart.lines[6].strokeColor = color07 + self.chart.lines[7].strokeColor = color08 + self.chart.lines[8].strokeColor = color09 + self.chart.lines[9].strokeColor = color10 + self.chart.fillColor = backgroundGrey + self.chart.lineLabels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontSize = 7 + self.chart.xValueAxis.forceZero = 0 + self.chart.data = [((100,100), (200,200), (250,210), (300,300), (400,500)), ((100,200), (200,300), (250,200), (300,400), (400, 600))] + self.chart.xValueAxis.avoidBoundFrac = 1 + self.chart.xValueAxis.gridEnd = 115 + self.chart.xValueAxis.tickDown = 3 + self.chart.xValueAxis.visibleGrid = 1 + self.chart.yValueAxis.tickLeft = 3 + self.chart.yValueAxis.labels.fontName = 'Helvetica' + self.chart.yValueAxis.labels.fontSize = 7 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self.chart.lineLabelFormat = None + self.chart.xLabel = 'X Axis' + self.chart.y = 30 + self.chart.yLabel = 'Y Axis' + self.chart.yValueAxis.labelTextFormat = '%d' + self.chart.yValueAxis.forceZero = 1 + self.chart.xValueAxis.forceZero = 1 + + + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + Scatter().save(formats=['pdf'],outDir=None,fnRoot='scatter') diff --git a/bin/reportlab/graphics/samples/scatter_lines.py b/bin/reportlab/graphics/samples/scatter_lines.py new file mode 100644 index 00000000000..16ed718b2fd --- /dev/null +++ b/bin/reportlab/graphics/samples/scatter_lines.py @@ -0,0 +1,82 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.charts.lineplots import ScatterPlot +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class ScatterLines(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,ScatterPlot(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.lines[0].strokeColor = color01 + self.chart.lines[1].strokeColor = color02 + self.chart.lines[2].strokeColor = color03 + self.chart.lines[3].strokeColor = color04 + self.chart.lines[4].strokeColor = color05 + self.chart.lines[5].strokeColor = color06 + self.chart.lines[6].strokeColor = color07 + self.chart.lines[7].strokeColor = color08 + self.chart.lines[8].strokeColor = color09 + self.chart.lines[9].strokeColor = color10 + self.chart.lines[0].symbol = None + self.chart.lines[1].symbol = None + self.chart.lines[2].symbol = None + self.chart.lines[3].symbol = None + self.chart.lines[4].symbol = None + self.chart.lines[5].symbol = None + self.chart.lines[6].symbol = None + self.chart.lines[7].symbol = None + self.chart.lines[8].symbol = None + self.chart.lines[9].symbol = None + self.chart.fillColor = backgroundGrey + self.chart.lineLabels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontSize = 7 + self.chart.xValueAxis.forceZero = 0 + self.chart.data = [((100,100), (200,200), (250,210), (300,300), (400,500)), ((100,200), (200,300), (250,200), (300,400), (400, 600))] + self.chart.xValueAxis.avoidBoundFrac = 1 + self.chart.xValueAxis.gridEnd = 115 + self.chart.xValueAxis.tickDown = 3 + self.chart.xValueAxis.visibleGrid = 1 + self.chart.yValueAxis.tickLeft = 3 + self.chart.yValueAxis.labels.fontName = 'Helvetica' + self.chart.yValueAxis.labels.fontSize = 7 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self.chart.lineLabelFormat = None + self.chart.xLabel = 'X Axis' + self.chart.y = 30 + self.chart.yLabel = 'Y Axis' + self.chart.yValueAxis.gridEnd = 115 + self.chart.yValueAxis.visibleGrid = 1 + self.chart.yValueAxis.labelTextFormat = '%d' + self.chart.yValueAxis.forceZero = 1 + self.chart.xValueAxis.forceZero = 1 + self.chart.joinedLines = 1 + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + ScatterLines().save(formats=['pdf'],outDir=None,fnRoot='scatter_lines') diff --git a/bin/reportlab/graphics/samples/scatter_lines_markers.py b/bin/reportlab/graphics/samples/scatter_lines_markers.py new file mode 100644 index 00000000000..34f8ff220f5 --- /dev/null +++ b/bin/reportlab/graphics/samples/scatter_lines_markers.py @@ -0,0 +1,72 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.charts.lineplots import ScatterPlot +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class ScatterLinesMarkers(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,ScatterPlot(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.lines[0].strokeColor = color01 + self.chart.lines[1].strokeColor = color02 + self.chart.lines[2].strokeColor = color03 + self.chart.lines[3].strokeColor = color04 + self.chart.lines[4].strokeColor = color05 + self.chart.lines[5].strokeColor = color06 + self.chart.lines[6].strokeColor = color07 + self.chart.lines[7].strokeColor = color08 + self.chart.lines[8].strokeColor = color09 + self.chart.lines[9].strokeColor = color10 + self.chart.fillColor = backgroundGrey + self.chart.lineLabels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontSize = 7 + self.chart.xValueAxis.forceZero = 0 + self.chart.data = [((100,100), (200,200), (250,210), (300,300), (400,500)), ((100,200), (200,300), (250,200), (300,400), (400, 600))] + self.chart.xValueAxis.avoidBoundFrac = 1 + self.chart.xValueAxis.gridEnd = 115 + self.chart.xValueAxis.tickDown = 3 + self.chart.xValueAxis.visibleGrid = 1 + self.chart.yValueAxis.tickLeft = 3 + self.chart.yValueAxis.labels.fontName = 'Helvetica' + self.chart.yValueAxis.labels.fontSize = 7 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self.chart.lineLabelFormat = None + self.chart.xLabel = 'X Axis' + self.chart.y = 30 + self.chart.yLabel = 'Y Axis' + self.chart.yValueAxis.gridEnd = 115 + self.chart.yValueAxis.visibleGrid = 1 + self.chart.yValueAxis.labelTextFormat = '%d' + self.chart.yValueAxis.forceZero = 1 + self.chart.xValueAxis.forceZero = 1 + self.chart.joinedLines = 1 + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + ScatterLinesMarkers().save(formats=['pdf'],outDir=None,fnRoot='scatter_lines_markers') diff --git a/bin/reportlab/graphics/samples/simple_pie.py b/bin/reportlab/graphics/samples/simple_pie.py new file mode 100644 index 00000000000..7542607f6ae --- /dev/null +++ b/bin/reportlab/graphics/samples/simple_pie.py @@ -0,0 +1,61 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.piecharts import Pie +from reportlab.graphics.widgets.grids import ShadedRect +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class SimplePie(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,Pie(),name='chart',validate=None,desc="The main chart") + self.chart.width = 100 + self.chart.height = 100 + self.chart.x = 25 + self.chart.y = 25 + self.chart.slices[0].fillColor = color01 + self.chart.slices[1].fillColor = color02 + self.chart.slices[2].fillColor = color03 + self.chart.slices[3].fillColor = color04 + self.chart.slices[4].fillColor = color05 + self.chart.slices[5].fillColor = color06 + self.chart.slices[6].fillColor = color07 + self.chart.slices[7].fillColor = color08 + self.chart.slices[8].fillColor = color09 + self.chart.slices[9].fillColor = color10 + self.chart.data = (100, 150, 180) + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'North'), (color02, 'South'),(color03, 'Central')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 160 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self.chart.slices.strokeWidth = 1 + self.chart.slices.fontName = 'Helvetica' + self.background = ShadedRect() + self.background.fillColorStart = backgroundGrey + self.background.fillColorEnd = backgroundGrey + self.background.numShades = 1 + self.background.strokeWidth = 0.5 + self.background.x = 25 + self.background.y = 25 + self.Legend.columnMaximum = 10 + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + SimplePie().save(formats=['pdf'],outDir=None,fnRoot=None) \ No newline at end of file diff --git a/bin/reportlab/graphics/samples/stacked_bar.py b/bin/reportlab/graphics/samples/stacked_bar.py new file mode 100644 index 00000000000..9ba2a962f82 --- /dev/null +++ b/bin/reportlab/graphics/samples/stacked_bar.py @@ -0,0 +1,85 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.charts.barcharts import HorizontalBarChart +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class StackedBar(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,HorizontalBarChart(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.bars[0].fillColor = color01 + self.chart.bars[1].fillColor = color02 + self.chart.bars[2].fillColor = color03 + self.chart.bars[3].fillColor = color04 + self.chart.bars[4].fillColor = color05 + self.chart.bars[5].fillColor = color06 + self.chart.bars[6].fillColor = color07 + self.chart.bars[7].fillColor = color08 + self.chart.bars[8].fillColor = color09 + self.chart.bars[9].fillColor = color10 + self.chart.fillColor = backgroundGrey + self.chart.barLabels.fontName = 'Helvetica' + self.chart.valueAxis.labels.fontName = 'Helvetica' + self.chart.valueAxis.labels.fontSize = 6 + self.chart.valueAxis.forceZero = 1 + self.chart.data = [(100, 150, 180), (125, 180, 200)] + self.chart.groupSpacing = 15 + self.chart.valueAxis.avoidBoundFrac = 1 + self.chart.valueAxis.gridEnd = 80 + self.chart.valueAxis.tickDown = 3 + self.chart.valueAxis.visibleGrid = 1 + self.chart.categoryAxis.categoryNames = ['North', 'South', 'Central'] + self.chart.categoryAxis.tickLeft = 3 + self.chart.categoryAxis.labels.fontName = 'Helvetica' + self.chart.categoryAxis.labels.fontSize = 6 + self.chart.categoryAxis.labels.dx = -3 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self._add(self,Label(),name='XLabel',validate=None,desc="The label on the horizontal axis") + self.XLabel.fontName = 'Helvetica' + self.XLabel.fontSize = 7 + self.XLabel.x = 85 + self.XLabel.y = 10 + self.XLabel.textAnchor ='middle' + self.XLabel.maxWidth = 100 + self.XLabel.height = 20 + self.XLabel._text = "X Axis" + self._add(self,Label(),name='YLabel',validate=None,desc="The label on the vertical axis") + self.YLabel.fontName = 'Helvetica' + self.YLabel.fontSize = 7 + self.YLabel.x = 12 + self.YLabel.y = 80 + self.YLabel.angle = 90 + self.YLabel.textAnchor ='middle' + self.YLabel.maxWidth = 100 + self.YLabel.height = 20 + self.YLabel._text = "Y Axis" + self.chart.categoryAxis.style='stacked' + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + StackedBar().save(formats=['pdf'],outDir=None,fnRoot='stacked_bar') \ No newline at end of file diff --git a/bin/reportlab/graphics/samples/stacked_column.py b/bin/reportlab/graphics/samples/stacked_column.py new file mode 100644 index 00000000000..a50a1598c90 --- /dev/null +++ b/bin/reportlab/graphics/samples/stacked_column.py @@ -0,0 +1,84 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.charts.barcharts import VerticalBarChart +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class StackedColumn(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,VerticalBarChart(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.bars[0].fillColor = color01 + self.chart.bars[1].fillColor = color02 + self.chart.bars[2].fillColor = color03 + self.chart.bars[3].fillColor = color04 + self.chart.bars[4].fillColor = color05 + self.chart.bars[5].fillColor = color06 + self.chart.bars[6].fillColor = color07 + self.chart.bars[7].fillColor = color08 + self.chart.bars[8].fillColor = color09 + self.chart.bars[9].fillColor = color10 + self.chart.fillColor = backgroundGrey + self.chart.barLabels.fontName = 'Helvetica' + self.chart.valueAxis.labels.fontName = 'Helvetica' + self.chart.valueAxis.labels.fontSize = 7 + self.chart.valueAxis.forceZero = 1 + self.chart.data = [(100, 150, 180), (125, 180, 200)] + self.chart.groupSpacing = 15 + self.chart.valueAxis.avoidBoundFrac = 1 + self.chart.valueAxis.gridEnd = 115 + self.chart.valueAxis.tickLeft = 3 + self.chart.valueAxis.visibleGrid = 1 + self.chart.categoryAxis.categoryNames = ['North', 'South', 'Central'] + self.chart.categoryAxis.tickDown = 3 + self.chart.categoryAxis.labels.fontName = 'Helvetica' + self.chart.categoryAxis.labels.fontSize = 7 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self._add(self,Label(),name='XLabel',validate=None,desc="The label on the horizontal axis") + self.XLabel.fontName = 'Helvetica' + self.XLabel.fontSize = 7 + self.XLabel.x = 85 + self.XLabel.y = 10 + self.XLabel.textAnchor ='middle' + self.XLabel.maxWidth = 100 + self.XLabel.height = 20 + self.XLabel._text = "X Axis" + self._add(self,Label(),name='YLabel',validate=None,desc="The label on the vertical axis") + self.YLabel.fontName = 'Helvetica' + self.YLabel.fontSize = 7 + self.YLabel.x = 12 + self.YLabel.y = 80 + self.YLabel.angle = 90 + self.YLabel.textAnchor ='middle' + self.YLabel.maxWidth = 100 + self.YLabel.height = 20 + self.YLabel._text = "Y Axis" + self.chart.categoryAxis.style='stacked' + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + StackedColumn().save(formats=['pdf'],outDir=None,fnRoot='stacked_column') diff --git a/bin/reportlab/graphics/shapes.py b/bin/reportlab/graphics/shapes.py new file mode 100644 index 00000000000..896385e8bee --- /dev/null +++ b/bin/reportlab/graphics/shapes.py @@ -0,0 +1,1289 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/shapes.py +""" +core of the graphics library - defines Drawing and Shapes +""" +__version__=''' $Id: shapes.py 2845 2006-05-03 12:24:35Z rgbecker $ ''' + +import string, os, sys +from math import pi, cos, sin, tan +from types import FloatType, IntType, ListType, TupleType, StringType, InstanceType +from pprint import pprint + +from reportlab.platypus import Flowable +from reportlab.rl_config import shapeChecking, verbose, defaultGraphicsFontName, _unset_ +from reportlab.lib import logger +from reportlab.lib import colors +from reportlab.lib.validators import * +from reportlab.lib.attrmap import * +from reportlab.lib.utils import fp_str +from reportlab.pdfbase.pdfmetrics import stringWidth + +class NotImplementedError(Exception): + pass + +# two constants for filling rules +NON_ZERO_WINDING = 'Non-Zero Winding' +EVEN_ODD = 'Even-Odd' + +## these can be overridden at module level before you start +#creating shapes. So, if using a special color model, +#this provides support for the rendering mechanism. +#you can change defaults globally before you start +#making shapes; one use is to substitute another +#color model cleanly throughout the drawing. + +STATE_DEFAULTS = { # sensible defaults for all + 'transform': (1,0,0,1,0,0), + + # styles follow SVG naming + 'strokeColor': colors.black, + 'strokeWidth': 1, + 'strokeLineCap': 0, + 'strokeLineJoin': 0, + 'strokeMiterLimit' : 'TBA', # don't know yet so let bomb here + 'strokeDashArray': None, + 'strokeOpacity': 1.0, #100% + + 'fillColor': colors.black, #...or text will be invisible + #'fillRule': NON_ZERO_WINDING, - these can be done later + #'fillOpacity': 1.0, #100% - can be done later + + 'fontSize': 10, + 'fontName': defaultGraphicsFontName, + 'textAnchor': 'start' # can be start, middle, end, inherited + } + + +#################################################################### +# math utilities. These could probably be moved into lib +# somewhere. +#################################################################### + +# constructors for matrices: +def nullTransform(): + return (1, 0, 0, 1, 0, 0) + +def translate(dx, dy): + return (1, 0, 0, 1, dx, dy) + +def scale(sx, sy): + return (sx, 0, 0, sy, 0, 0) + +def rotate(angle): + a = angle * pi/180 + return (cos(a), sin(a), -sin(a), cos(a), 0, 0) + +def skewX(angle): + a = angle * pi/180 + return (1, 0, tan(a), 1, 0, 0) + +def skewY(angle): + a = angle * pi/180 + return (1, tan(a), 0, 1, 0, 0) + +def mmult(A, B): + "A postmultiplied by B" + # I checked this RGB + # [a0 a2 a4] [b0 b2 b4] + # [a1 a3 a5] * [b1 b3 b5] + # [ 1 ] [ 1 ] + # + return (A[0]*B[0] + A[2]*B[1], + A[1]*B[0] + A[3]*B[1], + A[0]*B[2] + A[2]*B[3], + A[1]*B[2] + A[3]*B[3], + A[0]*B[4] + A[2]*B[5] + A[4], + A[1]*B[4] + A[3]*B[5] + A[5]) + +def inverse(A): + "For A affine 2D represented as 6vec return 6vec version of A**(-1)" + # I checked this RGB + det = float(A[0]*A[3] - A[2]*A[1]) + R = [A[3]/det, -A[1]/det, -A[2]/det, A[0]/det] + return tuple(R+[-R[0]*A[4]-R[2]*A[5],-R[1]*A[4]-R[3]*A[5]]) + +def zTransformPoint(A,v): + "Apply the homogenous part of atransformation a to vector v --> A*v" + return (A[0]*v[0]+A[2]*v[1],A[1]*v[0]+A[3]*v[1]) + +def transformPoint(A,v): + "Apply transformation a to vector v --> A*v" + return (A[0]*v[0]+A[2]*v[1]+A[4],A[1]*v[0]+A[3]*v[1]+A[5]) + +def transformPoints(matrix, V): + return map(transformPoint, V) + +def zTransformPoints(matrix, V): + return map(lambda x,matrix=matrix: zTransformPoint(matrix,x), V) + +def _textBoxLimits(text, font, fontSize, leading, textAnchor, boxAnchor): + w = 0 + for t in text: + w = max(w,stringWidth(t,font, fontSize)) + + h = len(text)*leading + yt = fontSize + if boxAnchor[0]=='s': + yb = -h + yt = yt - h + elif boxAnchor[0]=='n': + yb = 0 + else: + yb = -h/2.0 + yt = yt + yb + + if boxAnchor[-1]=='e': + xb = -w + if textAnchor=='end': xt = 0 + elif textAnchor=='start': xt = -w + else: xt = -w/2.0 + elif boxAnchor[-1]=='w': + xb = 0 + if textAnchor=='end': xt = w + elif textAnchor=='start': xt = 0 + else: xt = w/2.0 + else: + xb = -w/2.0 + if textAnchor=='end': xt = -xb + elif textAnchor=='start': xt = xb + else: xt = 0 + + return xb, yb, w, h, xt, yt + +def _rotatedBoxLimits( x, y, w, h, angle): + ''' + Find the corner points of the rotated w x h sized box at x,y + return the corner points and the min max points in the original space + ''' + C = zTransformPoints(rotate(angle),((x,y),(x+w,y),(x+w,y+h),(x,y+h))) + X = map(lambda x: x[0], C) + Y = map(lambda x: x[1], C) + return min(X), max(X), min(Y), max(Y), C + + +class _DrawTimeResizeable: + '''Addin class to provide the horribleness of _drawTimeResize''' + def _drawTimeResize(self,w,h): + if hasattr(self,'_canvas'): + canvas = self._canvas + drawing = canvas._drawing + drawing.width, drawing.height = w, h + if hasattr(canvas,'_drawTimeResize'): + canvas._drawTimeResize(w,h) + +class _SetKeyWordArgs: + def __init__(self, keywords={}): + """In general properties may be supplied to the constructor.""" + for key, value in keywords.items(): + setattr(self, key, value) + + +################################################################# +# +# Helper functions for working out bounds +# +################################################################# + +def getRectsBounds(rectList): + # filter out any None objects, e.g. empty groups + L = filter(lambda x: x is not None, rectList) + if not L: return None + + xMin, yMin, xMax, yMax = L[0] + for (x1, y1, x2, y2) in L[1:]: + if x1 < xMin: + xMin = x1 + if x2 > xMax: + xMax = x2 + if y1 < yMin: + yMin = y1 + if y2 > yMax: + yMax = y2 + return (xMin, yMin, xMax, yMax) + +def getPathBounds(points): + n = len(points) + f = lambda i,p = points: p[i] + xs = map(f,xrange(0,n,2)) + ys = map(f,xrange(1,n,2)) + return (min(xs), min(ys), max(xs), max(ys)) + +def getPointsBounds(pointList): + "Helper function for list of points" + first = pointList[0] + if type(first) in (ListType, TupleType): + xs = map(lambda xy: xy[0],pointList) + ys = map(lambda xy: xy[1],pointList) + return (min(xs), min(ys), max(xs), max(ys)) + else: + return getPathBounds(pointList) + +################################################################# +# +# And now the shapes themselves.... +# +################################################################# +class Shape(_SetKeyWordArgs,_DrawTimeResizeable): + """Base class for all nodes in the tree. Nodes are simply + packets of data to be created, stored, and ultimately + rendered - they don't do anything active. They provide + convenience methods for verification but do not + check attribiute assignments or use any clever setattr + tricks this time.""" + _attrMap = AttrMap() + + def copy(self): + """Return a clone of this shape.""" + + # implement this in the descendants as they need the right init methods. + raise NotImplementedError, "No copy method implemented for %s" % self.__class__.__name__ + + def getProperties(self,recur=1): + """Interface to make it easy to extract automatic + documentation""" + + #basic nodes have no children so this is easy. + #for more complex objects like widgets you + #may need to override this. + props = {} + for key, value in self.__dict__.items(): + if key[0:1] <> '_': + props[key] = value + return props + + def setProperties(self, props): + """Supports the bulk setting if properties from, + for example, a GUI application or a config file.""" + + self.__dict__.update(props) + #self.verify() + + def dumpProperties(self, prefix=""): + """Convenience. Lists them on standard output. You + may provide a prefix - mostly helps to generate code + samples for documentation.""" + + propList = self.getProperties().items() + propList.sort() + if prefix: + prefix = prefix + '.' + for (name, value) in propList: + print '%s%s = %s' % (prefix, name, value) + + def verify(self): + """If the programmer has provided the optional + _attrMap attribute, this checks all expected + attributes are present; no unwanted attributes + are present; and (if a checking function is found) + checks each attribute. Either succeeds or raises + an informative exception.""" + + if self._attrMap is not None: + for key in self.__dict__.keys(): + if key[0] <> '_': + assert self._attrMap.has_key(key), "Unexpected attribute %s found in %s" % (key, self) + for (attr, metavalue) in self._attrMap.items(): + assert hasattr(self, attr), "Missing attribute %s from %s" % (attr, self) + value = getattr(self, attr) + assert metavalue.validate(value), "Invalid value %s for attribute %s in class %s" % (value, attr, self.__class__.__name__) + + if shapeChecking: + """This adds the ability to check every attribute assignment as it is made. + It slows down shapes but is a big help when developing. It does not + get defined if rl_config.shapeChecking = 0""" + def __setattr__(self, attr, value): + """By default we verify. This could be off + in some parallel base classes.""" + validateSetattr(self,attr,value) #from reportlab.lib.attrmap + + def getBounds(self): + "Returns bounding rectangle of object as (x1,y1,x2,y2)" + raise NotImplementedError("Shapes and widgets must implement getBounds") + +class Group(Shape): + """Groups elements together. May apply a transform + to its contents. Has a publicly accessible property + 'contents' which may be used to iterate over contents. + In addition, child nodes may be given a name in which + case they are subsequently accessible as properties.""" + + _attrMap = AttrMap( + transform = AttrMapValue(isTransform,desc="Coordinate transformation to apply"), + contents = AttrMapValue(isListOfShapes,desc="Contained drawable elements"), + ) + + def __init__(self, *elements, **keywords): + """Initial lists of elements may be provided to allow + compact definitions in literal Python code. May or + may not be useful.""" + + # Groups need _attrMap to be an instance rather than + # a class attribute, as it may be extended at run time. + self._attrMap = self._attrMap.clone() + self.contents = [] + self.transform = (1,0,0,1,0,0) + for elt in elements: + self.add(elt) + # this just applies keywords; do it at the end so they + #don;t get overwritten + _SetKeyWordArgs.__init__(self, keywords) + + def _addNamedNode(self,name,node): + 'if name is not None add an attribute pointing to node and add to the attrMap' + if name: + if name not in self._attrMap.keys(): + self._attrMap[name] = AttrMapValue(isValidChild) + setattr(self, name, node) + + def add(self, node, name=None): + """Appends non-None child node to the 'contents' attribute. In addition, + if a name is provided, it is subsequently accessible by name + """ + # propagates properties down + if node is not None: + assert isValidChild(node), "Can only add Shape or UserNode objects to a Group" + self.contents.append(node) + self._addNamedNode(name,node) + + def _nn(self,node): + self.add(node) + return self.contents[-1] + + def insert(self, i, n, name=None): + 'Inserts sub-node n in contents at specified location' + if n is not None: + assert isValidChild(n), "Can only insert Shape or UserNode objects in a Group" + if i<0: + self.contents[i:i] =[n] + else: + self.contents.insert(i,n) + self._addNamedNode(name,n) + + def expandUserNodes(self): + """Return a new object which only contains primitive shapes.""" + + # many limitations - shared nodes become multiple ones, + obj = isinstance(self,Drawing) and Drawing(self.width,self.height) or Group() + obj._attrMap = self._attrMap.clone() + if hasattr(obj,'transform'): obj.transform = self.transform[:] + + self_contents = self.contents + a = obj.contents.append + for child in self_contents: + if isinstance(child, UserNode): + newChild = child.provideNode() + elif isinstance(child, Group): + newChild = child.expandUserNodes() + else: + newChild = child.copy() + a(newChild) + + self._copyNamedContents(obj) + return obj + + def _explode(self): + ''' return a fully expanded object''' + from reportlab.graphics.widgetbase import Widget + obj = Group() + if hasattr(obj,'transform'): obj.transform = self.transform[:] + P = self.contents[:] # pending nodes + while P: + n = P.pop(0) + if isinstance(n, UserNode): + P.append(n.provideNode()) + elif isinstance(n, Group): + n = n._explode() + if n.transform==(1,0,0,1,0,0): + obj.contents.extend(n.contents) + else: + obj.add(n) + else: + obj.add(n) + return obj + + def _copyContents(self,obj): + for child in self.contents: + obj.contents.append(child) + + def _copyNamedContents(self,obj,aKeys=None,noCopy=('contents',)): + from copy import copy + self_contents = self.contents + if not aKeys: aKeys = self._attrMap.keys() + for (k, v) in self.__dict__.items(): + if v in self_contents: + pos = self_contents.index(v) + setattr(obj, k, obj.contents[pos]) + elif k in aKeys and k not in noCopy: + setattr(obj, k, copy(v)) + + def _copy(self,obj): + """copies to obj""" + obj._attrMap = self._attrMap.clone() + self._copyContents(obj) + self._copyNamedContents(obj) + return obj + + def copy(self): + """returns a copy""" + return self._copy(self.__class__()) + + def rotate(self, theta): + """Convenience to help you set transforms""" + self.transform = mmult(self.transform, rotate(theta)) + + def translate(self, dx, dy): + """Convenience to help you set transforms""" + self.transform = mmult(self.transform, translate(dx, dy)) + + def scale(self, sx, sy): + """Convenience to help you set transforms""" + self.transform = mmult(self.transform, scale(sx, sy)) + + + def skew(self, kx, ky): + """Convenience to help you set transforms""" + self.transform = mmult(mmult(self.transform, skewX(kx)),skewY(ky)) + + def shift(self, x, y): + '''Convenience function to set the origin arbitrarily''' + self.transform = self.transform[:-2]+(x,y) + + def asDrawing(self, width, height): + """ Convenience function to make a drawing from a group + After calling this the instance will be a drawing! + """ + self.__class__ = Drawing + self._attrMap.update(self._xtraAttrMap) + self.width = width + self.height = height + + def getContents(self): + '''Return the list of things to be rendered + override to get more complicated behaviour''' + b = getattr(self,'background',None) + C = self.contents + if b and b not in C: C = [b]+C + return C + + def getBounds(self): + if self.contents: + b = [] + for elem in self.contents: + b.append(elem.getBounds()) + x1 = getRectsBounds(b) + if x1 is None: return None + x1, y1, x2, y2 = x1 + trans = self.transform + corners = [[x1,y1], [x1, y2], [x2, y1], [x2,y2]] + newCorners = [] + for corner in corners: + newCorners.append(transformPoint(trans, corner)) + return getPointsBounds(newCorners) + else: + #empty group needs a sane default; this + #will happen when interactively creating a group + #nothing has been added to yet. The alternative is + #to handle None as an allowed return value everywhere. + return None + +def _addObjImport(obj,I,n=None): + '''add an import of obj's class to a dictionary of imports''' #' + from inspect import getmodule + c = obj.__class__ + m = getmodule(c).__name__ + n = n or c.__name__ + if not I.has_key(m): + I[m] = [n] + elif n not in I[m]: + I[m].append(n) + +def _repr(self,I=None): + '''return a repr style string with named fixed args first, then keywords''' + if type(self) is InstanceType: + if self is EmptyClipPath: + _addObjImport(self,I,'EmptyClipPath') + return 'EmptyClipPath' + if I: _addObjImport(self,I) + if isinstance(self,Shape): + from inspect import getargs + args, varargs, varkw = getargs(self.__init__.im_func.func_code) + P = self.getProperties() + s = self.__class__.__name__+'(' + for n in args[1:]: + v = P[n] + del P[n] + s = s + '%s,' % _repr(v,I) + for n,v in P.items(): + v = P[n] + s = s + '%s=%s,' % (n, _repr(v,I)) + return s[:-1]+')' + else: + return repr(self) + elif type(self) is FloatType: + return fp_str(self) + elif type(self) in (ListType,TupleType): + s = '' + for v in self: + s = s + '%s,' % _repr(v,I) + if type(self) is ListType: + return '[%s]' % s[:-1] + else: + return '(%s%s)' % (s[:-1],len(self)==1 and ',' or '') + else: + return repr(self) + +def _renderGroupPy(G,pfx,I,i=0,indent='\t\t'): + s = '' + C = getattr(G,'transform',None) + if C: s = s + ('%s%s.transform = %s\n' % (indent,pfx,_repr(C))) + C = G.contents + for n in C: + if isinstance(n, Group): + npfx = 'v%d' % i + i = i + 1 + s = s + '%s%s=%s._nn(Group())\n' % (indent,npfx,pfx) + s = s + _renderGroupPy(n,npfx,I,i,indent) + i = i - 1 + else: + s = s + '%s%s.add(%s)\n' % (indent,pfx,_repr(n,I)) + return s + +def _extraKW(self,pfx,**kw): + kw.update(self.__dict__) + R = {} + n = len(pfx) + for k in kw.keys(): + if k.startswith(pfx): + R[k[n:]] = kw[k] + return R + +class Drawing(Group, Flowable): + """Outermost container; the thing a renderer works on. + This has no properties except a height, width and list + of contents.""" + + _xtraAttrMap = AttrMap( + width = AttrMapValue(isNumber,desc="Drawing width in points."), + height = AttrMapValue(isNumber,desc="Drawing height in points."), + canv = AttrMapValue(None), + background = AttrMapValue(isValidChildOrNone,desc="Background widget for the drawing"), + hAlign = AttrMapValue(OneOf("LEFT", "RIGHT", "CENTER", "CENTRE"), desc="Horizontal alignment within parent document"), + vAlign = AttrMapValue(OneOf("TOP", "BOTTOM", "CENTER", "CENTRE"), desc="Vertical alignment within parent document"), + #AR temporary hack to track back up. + #fontName = AttrMapValue(isStringOrNone), + renderScale = AttrMapValue(isNumber,desc="Global scaling for rendering"), + ) + + _attrMap = AttrMap(BASE=Group) + _attrMap.update(_xtraAttrMap) + + def __init__(self, width=400, height=200, *nodes, **keywords): + self.background = None + apply(Group.__init__,(self,)+nodes,keywords) + self.width = width + self.height = height + self.hAlign = 'LEFT' + self.vAlign = 'BOTTOM' + self.renderScale = 1.0 + + def _renderPy(self): + I = {'reportlab.graphics.shapes': ['_DrawingEditorMixin','Drawing','Group']} + G = _renderGroupPy(self._explode(),'self',I) + n = 'ExplodedDrawing_' + self.__class__.__name__ + s = '#Autogenerated by ReportLab guiedit do not edit\n' + for m, o in I.items(): + s = s + 'from %s import %s\n' % (m,string.replace(str(o)[1:-1],"'","")) + s = s + '\nclass %s(_DrawingEditorMixin,Drawing):\n' % n + s = s + '\tdef __init__(self,width=%s,height=%s,*args,**kw):\n' % (self.width,self.height) + s = s + '\t\tapply(Drawing.__init__,(self,width,height)+args,kw)\n' + s = s + G + s = s + '\n\nif __name__=="__main__": #NORUNTESTS\n\t%s().save(formats=[\'pdf\'],outDir=\'.\',fnRoot=None)\n' % n + return s + + def draw(self,showBoundary=_unset_): + """This is used by the Platypus framework to let the document + draw itself in a story. It is specific to PDF and should not + be used directly.""" + import renderPDF + renderPDF.draw(self, self.canv, 0, 0, showBoundary=showBoundary) + + def wrap(self, availWidth, availHeight): + width = self.width + height = self.height + renderScale = self.renderScale + if renderScale!=1.0: + width *= renderScale + height *= renderScale + return width, height + + def expandUserNodes(self): + """Return a new drawing which only contains primitive shapes.""" + obj = Group.expandUserNodes(self) + obj.width = self.width + obj.height = self.height + return obj + + def copy(self): + """Returns a copy""" + return self._copy(self.__class__(self.width, self.height)) + + def asGroup(self,*args,**kw): + return self._copy(apply(Group,args,kw)) + + def save(self, formats=None, verbose=None, fnRoot=None, outDir=None, title='', **kw): + """Saves copies of self in desired location and formats. + Multiple formats can be supported in one call + + the extra keywords can be of the form + _renderPM_dpi=96 (which passes dpi=96 to renderPM) + """ + from reportlab import rl_config + ext = '' + if not fnRoot: + fnRoot = getattr(self,'fileNamePattern',(self.__class__.__name__+'%03d')) + chartId = getattr(self,'chartId',0) + if callable(fnRoot): + fnRoot = fnRoot(chartId) + else: + try: + fnRoot = fnRoot % getattr(self,'chartId',0) + except TypeError, err: + #the exact error message changed from 2.2 to 2.3 so we need to + #check a substring + if str(err).find('not all arguments converted') < 0: raise + + if os.path.isabs(fnRoot): + outDir, fnRoot = os.path.split(fnRoot) + else: + outDir = outDir or getattr(self,'outDir','.') + outDir = outDir.rstrip().rstrip(os.sep) + if not outDir: outDir = '.' + if not os.path.isabs(outDir): outDir = os.path.join(getattr(self,'_override_CWD',os.path.dirname(sys.argv[0])),outDir) + if not os.path.isdir(outDir): os.makedirs(outDir) + fnroot = os.path.normpath(os.path.join(outDir,fnRoot)) + plotMode = os.path.splitext(fnroot) + if string.lower(plotMode[1][1:]) in ['pdf','ps','eps','gif','png','jpg','jpeg','pct','pict','tiff','tif','py','bmp','svg']: + fnroot = plotMode[0] + + plotMode, verbose = formats or getattr(self,'formats',['pdf']), (verbose is not None and (verbose,) or (getattr(self,'verbose',verbose),))[0] + _saved = logger.warnOnce.enabled, logger.infoOnce.enabled + logger.warnOnce.enabled = logger.infoOnce.enabled = verbose + if 'pdf' in plotMode: + from reportlab.graphics import renderPDF + filename = fnroot+'.pdf' + if verbose: print "generating PDF file %s" % filename + renderPDF.drawToFile(self, filename, title, showBoundary=getattr(self,'showBorder',rl_config.showBoundary),**_extraKW(self,'_renderPDF_',**kw)) + ext = ext + '/.pdf' + if sys.platform=='mac': + import macfs, macostools + macfs.FSSpec(filename).SetCreatorType("CARO", "PDF ") + macostools.touched(filename) + + for bmFmt in ['gif','png','tif','jpg','tiff','pct','pict', 'bmp']: + if bmFmt in plotMode: + from reportlab.graphics import renderPM + filename = '%s.%s' % (fnroot,bmFmt) + if verbose: print "generating %s file %s" % (bmFmt,filename) + renderPM.drawToFile(self, filename,fmt=bmFmt,showBoundary=getattr(self,'showBorder',rl_config.showBoundary),**_extraKW(self,'_renderPM_',**kw)) + ext = ext + '/.' + bmFmt + + if 'eps' in plotMode: + from rlextra.graphics import renderPS_SEP + filename = fnroot+'.eps' + if verbose: print "generating EPS file %s" % filename + renderPS_SEP.drawToFile(self, + filename, + title = fnroot, + dept = getattr(self,'EPS_info',['Testing'])[0], + company = getattr(self,'EPS_info',['','ReportLab'])[1], + preview = getattr(self,'preview',1), + showBoundary=getattr(self,'showBorder',rl_config.showBoundary),**_extraKW(self,'_renderPS_',**kw)) + ext = ext + '/.eps' + + if 'svg' in plotMode: + from reportlab.graphics import renderSVG + filename = fnroot+'.svg' + if verbose: print "generating EPS file %s" % filename + renderSVG.drawToFile(self, + filename, + showBoundary=getattr(self,'showBorder',rl_config.showBoundary),**_extraKW(self,'_renderSVG_',**kw)) + ext = ext + '/.svg' + + if 'ps' in plotMode: + from reportlab.graphics import renderPS + filename = fnroot+'.ps' + if verbose: print "generating EPS file %s" % filename + renderPS.drawToFile(self, filename, showBoundary=getattr(self,'showBorder',rl_config.showBoundary),**_extraKW(self,'_renderPS_',**kw)) + ext = ext + '/.ps' + + if 'py' in plotMode: + filename = fnroot+'.py' + if verbose: print "generating py file %s" % filename + open(filename,'w').write(self._renderPy()) + ext = ext + '/.py' + + logger.warnOnce.enabled, logger.infoOnce.enabled = _saved + if hasattr(self,'saveLogger'): + self.saveLogger(fnroot,ext) + return ext and fnroot+ext[1:] or '' + + + def asString(self, format, verbose=None, preview=0): + """Converts to an 8 bit string in given format.""" + assert format in ['pdf','ps','eps','gif','png','jpg','jpeg','bmp','ppm','tiff','tif','py','pict','pct'], 'Unknown file format "%s"' % format + from reportlab import rl_config + #verbose = verbose is not None and (verbose,) or (getattr(self,'verbose',verbose),)[0] + if format == 'pdf': + from reportlab.graphics import renderPDF + return renderPDF.drawToString(self) + elif format in ['gif','png','tif','jpg','pct','pict','bmp','ppm']: + from reportlab.graphics import renderPM + return renderPM.drawToString(self, fmt=format) + elif format == 'eps': + from rlextra.graphics import renderPS_SEP + return renderPS_SEP.drawToString(self, + preview = preview, + showBoundary=getattr(self,'showBorder',rl_config.showBoundary)) + elif format == 'ps': + from reportlab.graphics import renderPS + return renderPS.drawToString(self, showBoundary=getattr(self,'showBorder',rl_config.showBoundary)) + elif format == 'py': + return self._renderPy() + +class _DrawingEditorMixin: + '''This is a mixin to provide functionality for edited drawings''' + def _add(self,obj,value,name=None,validate=None,desc=None,pos=None): + ''' + effectively setattr(obj,name,value), but takes care of things with _attrMaps etc + ''' + ivc = isValidChild(value) + if name and hasattr(obj,'_attrMap'): + if not obj.__dict__.has_key('_attrMap'): + obj._attrMap = obj._attrMap.clone() + if ivc and validate is None: validate = isValidChild + obj._attrMap[name] = AttrMapValue(validate,desc) + if hasattr(obj,'add') and ivc: + if pos: + obj.insert(pos,value,name) + else: + obj.add(value,name) + elif name: + setattr(obj,name,value) + else: + raise ValueError, "Can't add, need name" + +class LineShape(Shape): + # base for types of lines + + _attrMap = AttrMap( + strokeColor = AttrMapValue(isColorOrNone), + strokeWidth = AttrMapValue(isNumber), + strokeLineCap = AttrMapValue(None), + strokeLineJoin = AttrMapValue(None), + strokeMiterLimit = AttrMapValue(isNumber), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone), + ) + + def __init__(self, kw): + self.strokeColor = STATE_DEFAULTS['strokeColor'] + self.strokeWidth = 1 + self.strokeLineCap = 0 + self.strokeLineJoin = 0 + self.strokeMiterLimit = 0 + self.strokeDashArray = None + self.setProperties(kw) + + +class Line(LineShape): + _attrMap = AttrMap(BASE=LineShape, + x1 = AttrMapValue(isNumber), + y1 = AttrMapValue(isNumber), + x2 = AttrMapValue(isNumber), + y2 = AttrMapValue(isNumber), + ) + + def __init__(self, x1, y1, x2, y2, **kw): + LineShape.__init__(self, kw) + self.x1 = x1 + self.y1 = y1 + self.x2 = x2 + self.y2 = y2 + + def getBounds(self): + "Returns bounding rectangle of object as (x1,y1,x2,y2)" + return (self.x1, self.y1, self.x2, self.y2) + + +class SolidShape(LineShape): + # base for anything with outline and content + + _attrMap = AttrMap(BASE=LineShape, + fillColor = AttrMapValue(isColorOrNone), + ) + + def __init__(self, kw): + self.fillColor = STATE_DEFAULTS['fillColor'] + # do this at the end so keywords overwrite + #the above settings + LineShape.__init__(self, kw) + + +# path operator constants +_MOVETO, _LINETO, _CURVETO, _CLOSEPATH = range(4) +_PATH_OP_ARG_COUNT = (2, 2, 6, 0) # [moveTo, lineTo, curveTo, closePath] +_PATH_OP_NAMES=['moveTo','lineTo','curveTo','closePath'] + +def _renderPath(path, drawFuncs): + """Helper function for renderers.""" + # this could be a method of Path... + points = path.points + i = 0 + hadClosePath = 0 + hadMoveTo = 0 + for op in path.operators: + nArgs = _PATH_OP_ARG_COUNT[op] + func = drawFuncs[op] + j = i + nArgs + apply(func, points[i:j]) + i = j + if op == _CLOSEPATH: + hadClosePath = hadClosePath + 1 + if op == _MOVETO: + hadMoveTo = hadMoveTo + 1 + return hadMoveTo == hadClosePath + +class Path(SolidShape): + """Path, made up of straight lines and bezier curves.""" + + _attrMap = AttrMap(BASE=SolidShape, + points = AttrMapValue(isListOfNumbers), + operators = AttrMapValue(isListOfNumbers), + isClipPath = AttrMapValue(isBoolean), + ) + + def __init__(self, points=None, operators=None, isClipPath=0, **kw): + SolidShape.__init__(self, kw) + if points is None: + points = [] + if operators is None: + operators = [] + assert len(points) % 2 == 0, 'Point list must have even number of elements!' + self.points = points + self.operators = operators + self.isClipPath = isClipPath + + def copy(self): + new = self.__class__(self.points[:], self.operators[:]) + new.setProperties(self.getProperties()) + return new + + def moveTo(self, x, y): + self.points.extend([x, y]) + self.operators.append(_MOVETO) + + def lineTo(self, x, y): + self.points.extend([x, y]) + self.operators.append(_LINETO) + + def curveTo(self, x1, y1, x2, y2, x3, y3): + self.points.extend([x1, y1, x2, y2, x3, y3]) + self.operators.append(_CURVETO) + + def closePath(self): + self.operators.append(_CLOSEPATH) + + def getBounds(self): + return getPathBounds(self.points) + +EmptyClipPath=Path() #special path + +def getArcPoints(centerx, centery, radius, startangledegrees, endangledegrees, yradius=None, degreedelta=None, reverse=None): + if yradius is None: yradius = radius + points = [] + from math import sin, cos, pi + degreestoradians = pi/180.0 + startangle = startangledegrees*degreestoradians + endangle = endangledegrees*degreestoradians + while endangle.001: + degreedelta = min(angle,degreedelta or 1.) + radiansdelta = degreedelta*degreestoradians + n = max(int(angle/radiansdelta+0.5),1) + radiansdelta = angle/n + n += 1 + else: + n = 1 + radiansdelta = 0 + + for angle in xrange(n): + angle = startangle+angle*radiansdelta + a((centerx+radius*cos(angle),centery+yradius*sin(angle))) + + if reverse: points.reverse() + return points + +class ArcPath(Path): + '''Path with an addArc method''' + def addArc(self, centerx, centery, radius, startangledegrees, endangledegrees, yradius=None, degreedelta=None, moveTo=None, reverse=None): + P = getArcPoints(centerx, centery, radius, startangledegrees, endangledegrees, yradius=yradius, degreedelta=degreedelta, reverse=reverse) + if moveTo or not len(self.operators): + self.moveTo(P[0][0],P[0][1]) + del P[0] + for x, y in P: self.lineTo(x,y) + +def definePath(pathSegs=[],isClipPath=0, dx=0, dy=0, **kw): + O = [] + P = [] + for seg in pathSegs: + if type(seg) not in [ListType,TupleType]: + opName = seg + args = [] + else: + opName = seg[0] + args = seg[1:] + if opName not in _PATH_OP_NAMES: + raise ValueError, 'bad operator name %s' % opName + op = _PATH_OP_NAMES.index(opName) + if len(args)!=_PATH_OP_ARG_COUNT[op]: + raise ValueError, '%s bad arguments %s' % (opName,str(args)) + O.append(op) + P.extend(list(args)) + for d,o in (dx,0), (dy,1): + for i in xrange(o,len(P),2): + P[i] = P[i]+d + return apply(Path,(P,O,isClipPath),kw) + +class Rect(SolidShape): + """Rectangle, possibly with rounded corners.""" + + _attrMap = AttrMap(BASE=SolidShape, + x = AttrMapValue(isNumber), + y = AttrMapValue(isNumber), + width = AttrMapValue(isNumber), + height = AttrMapValue(isNumber), + rx = AttrMapValue(isNumber), + ry = AttrMapValue(isNumber), + ) + + def __init__(self, x, y, width, height, rx=0, ry=0, **kw): + SolidShape.__init__(self, kw) + self.x = x + self.y = y + self.width = width + self.height = height + self.rx = rx + self.ry = ry + + def copy(self): + new = self.__class__(self.x, self.y, self.width, self.height) + new.setProperties(self.getProperties()) + return new + + def getBounds(self): + return (self.x, self.y, self.x + self.width, self.y + self.height) + + +class Image(SolidShape): + """Bitmap image.""" + + _attrMap = AttrMap(BASE=SolidShape, + x = AttrMapValue(isNumber), + y = AttrMapValue(isNumber), + width = AttrMapValue(isNumberOrNone), + height = AttrMapValue(isNumberOrNone), + path = AttrMapValue(None), + ) + + def __init__(self, x, y, width, height, path, **kw): + SolidShape.__init__(self, kw) + self.x = x + self.y = y + self.width = width + self.height = height + self.path = path + + def copy(self): + new = self.__class__(self.x, self.y, self.width, self.height, self.path) + new.setProperties(self.getProperties()) + return new + + def getBounds(self): + return (self.x, self.y, self.x + width, self.y + width) + +class Circle(SolidShape): + + _attrMap = AttrMap(BASE=SolidShape, + cx = AttrMapValue(isNumber), + cy = AttrMapValue(isNumber), + r = AttrMapValue(isNumber), + ) + + def __init__(self, cx, cy, r, **kw): + SolidShape.__init__(self, kw) + self.cx = cx + self.cy = cy + self.r = r + + def copy(self): + new = self.__class__(self.cx, self.cy, self.r) + new.setProperties(self.getProperties()) + return new + + def getBounds(self): + return (self.cx - self.r, self.cy - self.r, self.cx + self.r, self.cy + self.r) + +class Ellipse(SolidShape): + _attrMap = AttrMap(BASE=SolidShape, + cx = AttrMapValue(isNumber), + cy = AttrMapValue(isNumber), + rx = AttrMapValue(isNumber), + ry = AttrMapValue(isNumber), + ) + + def __init__(self, cx, cy, rx, ry, **kw): + SolidShape.__init__(self, kw) + self.cx = cx + self.cy = cy + self.rx = rx + self.ry = ry + + def copy(self): + new = self.__class__(self.cx, self.cy, self.rx, self.ry) + new.setProperties(self.getProperties()) + return new + + def getBounds(self): + return (self.cx - self.rx, self.cy - self.ry, self.cx + self.rx, self.cy + self.ry) + +class Wedge(SolidShape): + """A "slice of a pie" by default translates to a polygon moves anticlockwise + from start angle to end angle""" + + _attrMap = AttrMap(BASE=SolidShape, + centerx = AttrMapValue(isNumber), + centery = AttrMapValue(isNumber), + radius = AttrMapValue(isNumber), + startangledegrees = AttrMapValue(isNumber), + endangledegrees = AttrMapValue(isNumber), + yradius = AttrMapValue(isNumberOrNone), + radius1 = AttrMapValue(isNumberOrNone), + yradius1 = AttrMapValue(isNumberOrNone), + ) + + degreedelta = 1 # jump every 1 degrees + + def __init__(self, centerx, centery, radius, startangledegrees, endangledegrees, yradius=None, **kw): + SolidShape.__init__(self, kw) + while endangledegrees0.001: + degreedelta = min(self.degreedelta or 1.,angle) + radiansdelta = degreedelta*degreestoradians + n = max(1,int(angle/radiansdelta+0.5)) + radiansdelta = angle/n + n += 1 + else: + n = 1 + radiansdelta = 0 + CA = [] + CAA = CA.append + a = points.append + for angle in xrange(n): + angle = startangle+angle*radiansdelta + CAA((cos(angle),sin(angle))) + for c,s in CA: + a(centerx+radius*c) + a(centery+yradius*s) + if (radius1==0 or radius1 is None) and (yradius1==0 or yradius1 is None): + a(centerx); a(centery) + else: + CA.reverse() + for c,s in CA: + a(centerx+radius1*c) + a(centery+yradius1*s) + return Polygon(points) + + def copy(self): + new = self.__class__(self.centerx, + self.centery, + self.radius, + self.startangledegrees, + self.endangledegrees) + new.setProperties(self.getProperties()) + return new + + def getBounds(self): + return self.asPolygon().getBounds() + +class Polygon(SolidShape): + """Defines a closed shape; Is implicitly + joined back to the start for you.""" + + _attrMap = AttrMap(BASE=SolidShape, + points = AttrMapValue(isListOfNumbers), + ) + + def __init__(self, points=[], **kw): + SolidShape.__init__(self, kw) + assert len(points) % 2 == 0, 'Point list must have even number of elements!' + self.points = points + + def copy(self): + new = self.__class__(self.points) + new.setProperties(self.getProperties()) + return new + + def getBounds(self): + return getPointsBounds(self.points) + +class PolyLine(LineShape): + """Series of line segments. Does not define a + closed shape; never filled even if apparently joined. + Put the numbers in the list, not two-tuples.""" + + _attrMap = AttrMap(BASE=LineShape, + points = AttrMapValue(isListOfNumbers), + ) + + def __init__(self, points=[], **kw): + LineShape.__init__(self, kw) + lenPoints = len(points) + if lenPoints: + if type(points[0]) in (ListType,TupleType): + L = [] + for (x,y) in points: + L.append(x) + L.append(y) + points = L + else: + assert len(points) % 2 == 0, 'Point list must have even number of elements!' + self.points = points + + def copy(self): + new = self.__class__(self.points) + new.setProperties(self.getProperties()) + return new + + def getBounds(self): + return getPointsBounds(self.points) + +class String(Shape): + """Not checked against the spec, just a way to make something work. + Can be anchored left, middle or end.""" + + # to do. + _attrMap = AttrMap( + x = AttrMapValue(isNumber), + y = AttrMapValue(isNumber), + text = AttrMapValue(isString), + fontName = AttrMapValue(None), + fontSize = AttrMapValue(isNumber), + fillColor = AttrMapValue(isColorOrNone), + textAnchor = AttrMapValue(isTextAnchor), + encoding = AttrMapValue(isString), + ) + + def __init__(self, x, y, text, **kw): + self.x = x + self.y = y + self.text = text + self.textAnchor = 'start' + self.fontName = STATE_DEFAULTS['fontName'] + self.fontSize = STATE_DEFAULTS['fontSize'] + self.fillColor = STATE_DEFAULTS['fillColor'] + self.setProperties(kw) + self.encoding = 'cp1252' #matches only fonts we have! + + def getEast(self): + return self.x + stringWidth(self.text,self.fontName,self.fontSize, self.encoding) + + def copy(self): + new = self.__class__(self.x, self.y, self.text) + new.setProperties(self.getProperties()) + return new + + def getBounds(self): + # assumes constant drop of 0.2*size to baseline + w = stringWidth(self.text,self.fontName,self.fontSize, self.encoding) + if self.textAnchor == 'start': + x = self.x + elif self.textAnchor == 'middle': + x = self.x - 0.5*w + elif self.textAnchor == 'end': + x = self.x - w + return (x, self.y - 0.2 * self.fontSize, x+w, self.y + self.fontSize) + +class UserNode(_DrawTimeResizeable): + """A simple template for creating a new node. The user (Python + programmer) may subclasses this. provideNode() must be defined to + provide a Shape primitive when called by a renderer. It does + NOT inherit from Shape, as the renderer always replaces it, and + your own classes can safely inherit from it without getting + lots of unintended behaviour.""" + + def provideNode(self): + """Override this to create your own node. This lets widgets be + added to drawings; they must create a shape (typically a group) + so that the renderer can draw the custom node.""" + + raise NotImplementedError, "this method must be redefined by the user/programmer" + + +def test(): + r = Rect(10,10,200,50) + import pprint + pp = pprint.pprint + print 'a Rectangle:' + pp(r.getProperties()) + print + print 'verifying...', + r.verify() + print 'OK' + #print 'setting rect.z = "spam"' + #r.z = 'spam' + print 'deleting rect.width' + del r.width + print 'verifying...', + r.verify() + + +if __name__=='__main__': + test() diff --git a/bin/reportlab/graphics/testdrawings.py b/bin/reportlab/graphics/testdrawings.py new file mode 100644 index 00000000000..d5c52fc57eb --- /dev/null +++ b/bin/reportlab/graphics/testdrawings.py @@ -0,0 +1,294 @@ +#!/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/testdrawings.py +__version__=''' $Id $ ''' +"""This contains a number of routines to generate test drawings +for reportlab/graphics. For now they are contrived, but we will expand them +to try and trip up any parser. Feel free to add more. + +""" + +from reportlab.graphics.shapes import * +from reportlab.lib import colors + +def getDrawing1(): + """Hello World, on a rectangular background""" + + D = Drawing(400, 200) + D.add(Rect(50, 50, 300, 100, fillColor=colors.yellow)) #round corners + D.add(String(180,100, 'Hello World', fillColor=colors.red)) + + + return D + + +def getDrawing2(): + """This demonstrates the basic shapes. There are + no groups or references. Each solid shape should have + a purple fill.""" + D = Drawing(400, 200) #, fillColor=colors.purple) + + D.add(Line(10,10,390,190)) + D.add(Circle(100,100,20, fillColor=colors.purple)) + D.add(Circle(200,100,20, fillColor=colors.purple)) + D.add(Circle(300,100,20, fillColor=colors.purple)) + + D.add(Wedge(330,100,40, -10,40, fillColor=colors.purple)) + + D.add(PolyLine([120,10,130,20,140,10,150,20,160,10, + 170,20,180,10,190,20,200,10])) + + D.add(Polygon([300,20,350,20,390,80,300,75, 330, 40])) + + D.add(Ellipse(50, 150, 40, 20)) + + D.add(Rect(120, 150, 60, 30, + strokeWidth=10, + strokeColor=colors.red, + fillColor=colors.yellow)) #square corners + + D.add(Rect(220, 150, 60, 30, 10, 10)) #round corners + + D.add(String(10,50, 'Basic Shapes', fillColor=colors.black)) + + return D + + +##def getDrawing2(): +## """This drawing uses groups. Each group has two circles and a comment. +## The line style is set at group level and should be red for the left, +## bvlue for the right.""" +## D = Drawing(400, 200) +## +## Group1 = Group() +## +## Group1.add(String(50, 50, 'Group 1', fillColor=colors.black)) +## Group1.add(Circle(75,100,25)) +## Group1.add(Circle(125,100,25)) +## D.add(Group1) +## +## Group2 = Group( +## String(250, 50, 'Group 2', fillColor=colors.black), +## Circle(275,100,25), +## Circle(325,100,25)#, + + +##def getDrawing2(): +## """This drawing uses groups. Each group has two circles and a comment. +## The line style is set at group level and should be red for the left, +## bvlue for the right.""" +## D = Drawing(400, 200) +## +## Group1 = Group() +## +## Group1.add(String(50, 50, 'Group 1', fillColor=colors.black)) +## Group1.add(Circle(75,100,25)) +## Group1.add(Circle(125,100,25)) +## D.add(Group1) +## +## Group2 = Group( +## String(250, 50, 'Group 2', fillColor=colors.black), +## Circle(275,100,25), +## Circle(325,100,25)#, +## +## #group attributes +## #strokeColor=colors.blue +## ) +## D.add(Group2) + +## return D +## +## +##def getDrawing3(): +## """This uses a named reference object. The house is a 'subroutine' +## the basic brick colored walls are defined, but the roof and window +## color are undefined and may be set by the container.""" +## +## D = Drawing(400, 200, fill=colors.bisque) +## +## +## House = Group( +## Rect(2,20,36,30, fill=colors.bisque), #walls +## Polygon([0,20,40,20,20,5]), #roof +## Rect(8, 38, 8, 12), #door +## Rect(25, 38, 8, 7), #window +## Rect(8, 25, 8, 7), #window +## Rect(25, 25, 8, 7) #window +## +## ) +## D.addDef('MyHouse', House) +## +## # one row all the same color +## D.add(String(20, 40, 'British Street...',fill=colors.black)) +## for i in range(6): +## x = i * 50 +## D.add(NamedReference('MyHouse', +## House, +## transform=translate(x, 40), +## fill = colors.brown +## ) +## ) +## +## # now do a row all different +## D.add(String(20, 120, 'Mediterranean Street...',fill=colors.black)) +## x = 0 +## for color in (colors.blue, colors.yellow, colors.orange, +## colors.red, colors.green, colors.chartreuse): +## D.add(NamedReference('MyHouse', +## House, +## transform=translate(x,120), +## fill = color, +## ) +## ) +## x = x + 50 +## #..by popular demand, the mayor gets a big one at the end +## D.add(NamedReference('MyHouse', +## House, +## transform=mmult(translate(x,110), scale(1.2,1.2)), +## fill = color, +## ) +## ) +## +## +## return D +## +##def getDrawing4(): +## """This tests that attributes are 'unset' correctly when +## one steps back out of a drawing node. All the circles are part of a +## group setting the line color to blue; the second circle explicitly +## sets it to red. Ideally, the third circle should go back to blue.""" +## D = Drawing(400, 200) +## +## +## G = Group( +## Circle(100,100,20), +## Circle(200,100,20, stroke=colors.blue), +## Circle(300,100,20), +## stroke=colors.red, +## stroke_width=3, +## fill=colors.aqua +## ) +## D.add(G) +## +## +## D.add(String(10,50, 'Stack Unwinding - should be red, blue, red')) +## +## return D +## +## +##def getDrawing5(): +## """This Rotates Coordinate Axes""" +## D = Drawing(400, 200) +## +## +## +## Axis = Group( +## Line(0,0,100,0), #x axis +## Line(0,0,0,50), # y axis +## Line(0,10,10,10), #ticks on y axis +## Line(0,20,10,20), +## Line(0,30,10,30), +## Line(0,40,10,40), +## Line(10,0,10,10), #ticks on x axis +## Line(20,0,20,10), +## Line(30,0,30,10), +## Line(40,0,40,10), +## Line(50,0,50,10), +## Line(60,0,60,10), +## Line(70,0,70,10), +## Line(80,0,80,10), +## Line(90,0,90,10), +## String(20, 35, 'Axes', fill=colors.black) +## ) +## +## D.addDef('Axes', Axis) +## +## D.add(NamedReference('Axis', Axis, +## transform=translate(10,10))) +## D.add(NamedReference('Axis', Axis, +## transform=mmult(translate(150,10),rotate(15))) +## ) +## return D +## +##def getDrawing6(): +## """This Rotates Text""" +## D = Drawing(400, 300, fill=colors.black) +## +## xform = translate(200,150) +## C = (colors.black,colors.red,colors.green,colors.blue,colors.brown,colors.gray, colors.pink, +## colors.lavender,colors.lime, colors.mediumblue, colors.magenta, colors.limegreen) +## +## for i in range(12): +## D.add(String(0, 0, ' - - Rotated Text', fill=C[i%len(C)], transform=mmult(xform, rotate(30*i)))) +## +## return D +## +##def getDrawing7(): +## """This defines and tests a simple UserNode0 (the trailing zero denotes +## an experimental method which is not part of the supported API yet). +## Each of the four charts is a subclass of UserNode which generates a random +## series when rendered.""" +## +## class MyUserNode(UserNode0): +## import whrandom, math +## +## +## def provideNode(self, sender): +## """draw a simple chart that changes everytime it's drawn""" +## # print "here's a random number %s" % self.whrandom.random() +## #print "MyUserNode.provideNode being called by %s" % sender +## g = Group() +## #g._state = self._state # this is naughty +## PingoNode.__init__(g, self._state) # is this less naughty ? +## w = 80.0 +## h = 50.0 +## g.add(Rect(0,0, w, h, stroke=colors.black)) +## N = 10.0 +## x,y = (0,h) +## dx = w/N +## for ii in range(N): +## dy = (h/N) * self.whrandom.random() +## g.add(Line(x,y,x+dx, y-dy)) +## x = x + dx +## y = y - dy +## return g +## +## D = Drawing(400,200, fill=colors.white) # AR - same size as others +## +## D.add(MyUserNode()) +## +## graphcolor= [colors.green, colors.red, colors.brown, colors.purple] +## for ii in range(4): +## D.add(Group( MyUserNode(stroke=graphcolor[ii], stroke_width=2), +## transform=translate(ii*90,0) )) +## +## #un = MyUserNode() +## #print un.provideNode() +## return D +## +##def getDrawing8(): +## """Test Path operations--lineto, curveTo, etc.""" +## D = Drawing(400, 200, fill=None, stroke=colors.purple, stroke_width=2) +## +## xform = translate(200,100) +## C = (colors.black,colors.red,colors.green,colors.blue,colors.brown,colors.gray, colors.pink, +## colors.lavender,colors.lime, colors.mediumblue, colors.magenta, colors.limegreen) +## p = Path(50,50) +## p.lineTo(100,100) +## p.moveBy(-25,25) +## p.curveTo(150,125, 125,125, 200,50) +## p.curveTo(175, 75, 175, 98, 62, 87) +## +## +## D.add(p) +## D.add(String(10,30, 'Tests of path elements-lines and bezier curves-and text formating')) +## D.add(Line(220,150, 220,200, stroke=colors.red)) +## D.add(String(220,180, "Text should be centered", text_anchor="middle") ) +## +## +## return D + + +if __name__=='__main__': + print __doc__ \ No newline at end of file diff --git a/bin/reportlab/graphics/testshapes.py b/bin/reportlab/graphics/testshapes.py new file mode 100644 index 00000000000..261d7d73d34 --- /dev/null +++ b/bin/reportlab/graphics/testshapes.py @@ -0,0 +1,574 @@ +#!/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/testshapes.py + +# testshapes.py - draws shapes onto a PDF canvas. + +""" +Execute the script to see some test drawings. + +This contains a number of routines to generate test drawings +for reportlab/graphics. For now many of them are contrived, +but we will expand them to try and trip up any parser. +Feel free to add more. +""" + +__version__ = ''' $Id $ ''' + + +import os, sys + +from reportlab.lib import colors +from reportlab.lib.units import cm +from reportlab.pdfgen.canvas import Canvas +from reportlab.pdfbase.pdfmetrics import stringWidth +from reportlab.platypus import Flowable +from reportlab.graphics.shapes import * +from reportlab.graphics.renderPDF import _PDFRenderer +import unittest + +_FONTS = ['Times-Roman','Courier','Times-BoldItalic',] + +######################################################### +# +# Collections of shape drawings. +# +######################################################### + + +def getFailedDrawing(funcName): + """Generate a drawing in case something goes really wrong. + + This will create a drawing to be displayed whenever some + other drawing could not be executed, because the generating + function does something terribly wrong! The box contains + an attention triangle, plus some error message. + """ + + D = Drawing(400, 200) + + points = [200,170, 140,80, 260,80] + D.add(Polygon(points, + strokeWidth=0.5*cm, + strokeColor=colors.red, + fillColor=colors.yellow)) + + s = String(200, 40, + "Error in generating function '%s'!" % funcName, + textAnchor='middle') + D.add(s) + + return D + + +# These are the real drawings to be eye-balled. + +def getDrawing01(): + """Hello World, on a rectangular background. + + The rectangle's fillColor is yellow. + The string's fillColor is red. + """ + + D = Drawing(400, 200) + D.add(Rect(50, 50, 300, 100, fillColor=colors.yellow)) + D.add(String(180,100, 'Hello World', fillColor=colors.red)) + D.add(String(180,86, 'Some special characters \xc2\xa2\xc2\xa9\xc2\xae\xc2\xa3\xca\xa5\xd0\x96\xd6\x83\xd7\x90\xd9\x82\xe0\xa6\x95\xce\xb1\xce\xb2\xce\xb3', fillColor=colors.red)) + + return D + + +def getDrawing02(): + """Various Line shapes. + + The lines are blue and their strokeWidth is 5 mm. + One line has a strokeDashArray set to [5, 10, 15]. + """ + + D = Drawing(400, 200) + D.add(Line(50,50, 300,100, + strokeColor=colors.blue, + strokeWidth=0.5*cm, + )) + D.add(Line(50,100, 300,50, + strokeColor=colors.blue, + strokeWidth=0.5*cm, + strokeDashArray=[5, 10, 15], + )) + + #x = 1/0 # Comment this to see the actual drawing! + + return D + + +def getDrawing03(): + """Text strings in various sizes and different fonts. + + Font size increases from 12 to 36 and from bottom left + to upper right corner. The first ones should be in + Times-Roman. Finally, a solitary Courier string at + the top right corner. + """ + + D = Drawing(400, 200) + for size in range(12, 36, 4): + D.add(String(10+size*2, + 10+size*2, + 'Hello World', + fontName=_FONTS[0], + fontSize=size)) + + D.add(String(150, 150, + 'Hello World', + fontName=_FONTS[1], + fontSize=36)) + return D + + +def getDrawing04(): + """Text strings in various colours. + + Colours are blue, yellow and red from bottom left + to upper right. + """ + + D = Drawing(400, 200) + i = 0 + for color in (colors.blue, colors.yellow, colors.red): + D.add(String(50+i*30, 50+i*30, + 'Hello World', fillColor=color)) + i = i + 1 + + return D + + +def getDrawing05(): + """Text strings with various anchors (alignments). + + Text alignment conforms to the anchors in the left column. + """ + + D = Drawing(400, 200) + + lineX = 250 + D.add(Line(lineX,10, lineX,190, strokeColor=colors.gray)) + + y = 130 + for anchor in ('start', 'middle', 'end'): + D.add(String(lineX, y, 'Hello World', textAnchor=anchor)) + D.add(String(50, y, anchor + ':')) + y = y - 30 + + return D + + +def getDrawing06(): + """This demonstrates all the basic shapes at once. + + There are no groups or references. + Each solid shape should have a purple fill. + """ + + purple = colors.purple + purple = colors.green + + D = Drawing(400, 200) #, fillColor=purple) + + D.add(Line(10,10, 390,190)) + + D.add(Circle(100,100,20, fillColor=purple)) + D.add(Circle(200,100,40, fillColor=purple)) + D.add(Circle(300,100,30, fillColor=purple)) + + D.add(Wedge(330,100,40, -10,40, fillColor=purple)) + + D.add(PolyLine([120,10, 130,20, 140,10, 150,20, 160,10, + 170,20, 180,10, 190,20, 200,10], fillColor=purple)) + + D.add(Polygon([300,20, 350,20, 390,80, 300,75, 330,40], fillColor=purple)) + + D.add(Ellipse(50,150, 40, 20, fillColor=purple)) + + D.add(Rect(120,150, 60,30, + strokeWidth=10, + strokeColor=colors.yellow, + fillColor=purple)) #square corners + + D.add(Rect(220, 150, 60, 30, 10, 10, fillColor=purple)) #round corners + + from reportlab.lib.validators import inherit + D.add(String(10,50, 'Basic Shapes', fillColor=colors.black, fontName=inherit)) + + return D + +def getDrawing07(): + """This tests the ability to translate and rotate groups. The first set of axes should be + near the bottom left of the drawing. The second should be rotated counterclockwise + by 15 degrees. The third should be rotated by 30 degrees.""" + D = Drawing(400, 200) + + Axis = Group( + Line(0,0,100,0), #x axis + Line(0,0,0,50), # y axis + Line(0,10,10,10), #ticks on y axis + Line(0,20,10,20), + Line(0,30,10,30), + Line(0,40,10,40), + Line(10,0,10,10), #ticks on x axis + Line(20,0,20,10), + Line(30,0,30,10), + Line(40,0,40,10), + Line(50,0,50,10), + Line(60,0,60,10), + Line(70,0,70,10), + Line(80,0,80,10), + Line(90,0,90,10), + String(20, 35, 'Axes', fill=colors.black) + ) + + firstAxisGroup = Group(Axis) + firstAxisGroup.translate(10,10) + D.add(firstAxisGroup) + + secondAxisGroup = Group(Axis) + secondAxisGroup.translate(150,10) + secondAxisGroup.rotate(15) + + D.add(secondAxisGroup) + + + thirdAxisGroup = Group(Axis, transform=mmult(translate(300,10), rotate(30))) + D.add(thirdAxisGroup) + + return D + + +def getDrawing08(): + """This tests the ability to scale coordinates. The bottom left set of axes should be + near the bottom left of the drawing. The bottom right should be stretched vertically + by a factor of 2. The top left one should be stretched horizontally by a factor of 2. + The top right should have the vertical axiss leaning over to the right by 30 degrees.""" + D = Drawing(400, 200) + + Axis = Group( + Line(0,0,100,0), #x axis + Line(0,0,0,50), # y axis + Line(0,10,10,10), #ticks on y axis + Line(0,20,10,20), + Line(0,30,10,30), + Line(0,40,10,40), + Line(10,0,10,10), #ticks on x axis + Line(20,0,20,10), + Line(30,0,30,10), + Line(40,0,40,10), + Line(50,0,50,10), + Line(60,0,60,10), + Line(70,0,70,10), + Line(80,0,80,10), + Line(90,0,90,10), + String(20, 35, 'Axes', fill=colors.black) + ) + + firstAxisGroup = Group(Axis) + firstAxisGroup.translate(10,10) + D.add(firstAxisGroup) + + secondAxisGroup = Group(Axis) + secondAxisGroup.translate(150,10) + secondAxisGroup.scale(1,2) + D.add(secondAxisGroup) + + thirdAxisGroup = Group(Axis) + thirdAxisGroup.translate(10,125) + thirdAxisGroup.scale(2,1) + D.add(thirdAxisGroup) + + fourthAxisGroup = Group(Axis) + fourthAxisGroup.translate(250,125) + fourthAxisGroup.skew(30,0) + D.add(fourthAxisGroup) + + + return D + +def getDrawing09(): + """This tests rotated strings + + Some renderers will have a separate mechanism for font drawing. This test + just makes sure strings get transformed the same way as regular graphics.""" + D = Drawing(400, 200) + + fontName = _FONTS[0] + fontSize = 12 + text = "I should be totally horizontal and enclosed in a box" + textWidth = stringWidth(text, fontName, fontSize) + + + g1 = Group( + String(20, 20, text, fontName=fontName, fontSize = fontSize), + Rect(18, 18, textWidth + 4, fontSize + 4, fillColor=None) + ) + D.add(g1) + + text = "I should slope up by 15 degrees, so my right end is higher than my left" + textWidth = stringWidth(text, fontName, fontSize) + g2 = Group( + String(20, 20, text, fontName=fontName, fontSize = fontSize), + Rect(18, 18, textWidth + 4, fontSize + 4, fillColor=None) + ) + g2.translate(0, 50) + g2.rotate(15) + D.add(g2) + + return D + +def getDrawing10(): + """This tests nested groups with multiple levels of coordinate transformation. + Each box should be staggered up and to the right, moving by 25 points each time.""" + D = Drawing(400, 200) + + fontName = _FONTS[0] + fontSize = 12 + + g1 = Group( + Rect(0, 0, 100, 20, fillColor=colors.yellow), + String(5, 5, 'Text in the box', fontName=fontName, fontSize = fontSize) + ) + D.add(g1) + + g2 = Group(g1, transform = translate(25,25)) + D.add(g2) + + g3 = Group(g2, transform = translate(25,25)) + D.add(g3) + + g4 = Group(g3, transform = translate(25,25)) + D.add(g4) + + + return D + +from widgets.signsandsymbols import SmileyFace +def getDrawing11(): + '''test of anchoring''' + def makeSmiley(x, y, size, color): + "Make a smiley data item representation." + d = size + s = SmileyFace() + s.fillColor = color + s.x = x-d + s.y = y-d + s.size = d*2 + return s + + D = Drawing(400, 200) #, fillColor=colors.purple) + g = Group(transform=(1,0,0,1,0,0)) + g.add(makeSmiley(100,100,10,colors.red)) + g.add(Line(90,100,110,100,strokeColor=colors.green)) + g.add(Line(100,90,100,110,strokeColor=colors.green)) + D.add(g) + g = Group(transform=(2,0,0,2,100,-100)) + g.add(makeSmiley(100,100,10,colors.blue)) + g.add(Line(90,100,110,100,strokeColor=colors.green)) + g.add(Line(100,90,100,110,strokeColor=colors.green)) + D.add(g) + g = Group(transform=(2,0,0,2,0,0)) + return D + + +def getDrawing12(): + """Text strings in a non-standard font. + All that is required is to place the .afm and .pfb files + on the font patch given in rl_config.py, + for example in reportlab/lib/fonts/. + """ + faceName = "LettErrorRobot-Chrome" + D = Drawing(400, 200) + for size in range(12, 36, 4): + D.add(String(10+size*2, + 10+size*2, + 'Hello World', + fontName=faceName, + fontSize=size)) + return D + +def getDrawing13(): + 'Test Various TTF Fonts' + from reportlab.pdfbase import pdfmetrics, ttfonts + pdfmetrics.registerFont(ttfonts.TTFont("LuxiSerif", "luxiserif.ttf")) + pdfmetrics.registerFont(ttfonts.TTFont("Rina", "rina.ttf")) + _FONTS[1] = 'LuxiSerif' + _FONTS[2] = 'Rina' + F = ['Times-Roman','Courier','Helvetica','LuxiSerif', 'Rina'] + if sys.platform=='win32': + for name, ttf in [('Adventurer Light SF','Advlit.ttf'),('ArialMS','ARIAL.TTF'), + ('Arial Unicode MS', 'ARIALUNI.TTF'), + ('Book Antiqua','BKANT.TTF'), + ('Century Gothic','GOTHIC.TTF'), + ('Comic Sans MS', 'COMIC.TTF'), + ('Elementary Heavy SF Bold','Vwagh.ttf'), + ('Firenze SF','flot.ttf'), + ('Garamond','GARA.TTF'), + ('Jagger','Rols.ttf'), + ('Monotype Corsiva','MTCORSVA.TTF'), + ('Seabird SF','seag.ttf'), + ('Tahoma','TAHOMA.TTF'), + ('VerdanaMS','VERDANA.TTF'), + ]: + for D in ('c:\WINNT','c:\Windows'): + fn = os.path.join(D,'Fonts',ttf) + if os.path.isfile(fn): + try: + f = ttfonts.TTFont(name, fn) + pdfmetrics.registerFont(f) + F.append(name) + except: + pass + + def drawit(F,w=400,h=200,fontSize=12,slack=2,gap=5): + D = Drawing(w,h) + th = 2*gap + fontSize*1.2 + gh = gap + .2*fontSize + y = h + maxx = 0 + for fontName in F: + y -= th + text = fontName+": I should be totally horizontal and enclosed in a box and end in alphabetagamma \xc2\xa2\xc2\xa9\xc2\xae\xc2\xa3\xca\xa5\xd0\x96\xd6\x83\xd7\x90\xd9\x82\xe0\xa6\x95\xce\xb1\xce\xb2\xce\xb3" + textWidth = stringWidth(text, fontName, fontSize) + maxx = max(maxx,textWidth+20) + D.add( + Group(Rect(8, y-gh, textWidth + 4, th, strokeColor=colors.red, strokeWidth=.5, fillColor=colors.lightgrey), + String(10, y, text, fontName=fontName, fontSize = fontSize))) + y -= 5 + return maxx, h-y+gap, D + maxx, maxy, D = drawit(F) + if maxx>400 or maxy>200: _,_,D = drawit(F,maxx,maxy) + return D + +##def getDrawing14(): +## """This tests inherited properties. Each font should be as it says.""" +## D = Drawing(400, 200) +## +## fontSize = 12 +## D.fontName = 'Courier' +## +## g1 = Group( +## Rect(0, 0, 150, 20, fillColor=colors.yellow), +## String(5, 5, 'Inherited Courier', fontName=inherit, fontSize = fontSize) +## ) +## D.add(g1) +## +## g2 = Group(g1, transform = translate(25,25)) +## D.add(g2) +## +## g3 = Group(g2, transform = translate(25,25)) +## D.add(g3) +## +## g4 = Group(g3, transform = translate(25,25)) +## D.add(g4) +## +## +## return D +def getAllFunctionDrawingNames(doTTF=1): + "Get a list of drawing function names from somewhere." + + funcNames = [] + + # Here we get the names from the global name space. + symbols = globals().keys() + symbols.sort() + for funcName in symbols: + if funcName[0:10] == 'getDrawing': + if doTTF or funcName!='getDrawing13': + funcNames.append(funcName) + + return funcNames + +def _evalFuncDrawing(name, D, l=None, g=None): + try: + d = eval(name + '()', g or globals(), l or locals()) + except: + d = getFailedDrawing(name) + D.append((d, eval(name + '.__doc__'), name[3:])) + +def getAllTestDrawings(doTTF=1): + D = [] + for f in getAllFunctionDrawingNames(doTTF=doTTF): + _evalFuncDrawing(f,D) + return D + +def writePDF(drawings): + "Create and save a PDF file containing some drawings." + + pdfPath = os.path.splitext(sys.argv[0])[0] + '.pdf' + c = Canvas(pdfPath) + c.setFont(_FONTS[0], 32) + c.drawString(80, 750, 'ReportLab Graphics-Shapes Test') + + # Print drawings in a loop, with their doc strings. + c.setFont(_FONTS[0], 12) + y = 740 + i = 1 + for (drawing, docstring, funcname) in drawings: + if y < 300: # Allows 5-6 lines of text. + c.showPage() + y = 740 + # Draw a title. + y = y - 30 + c.setFont(_FONTS[2],12) + c.drawString(80, y, '%s (#%d)' % (funcname, i)) + c.setFont(_FONTS[0],12) + y = y - 14 + textObj = c.beginText(80, y) + textObj.textLines(docstring) + c.drawText(textObj) + y = textObj.getY() + y = y - drawing.height + drawing.drawOn(c, 80, y) + i = i + 1 + + c.save() + print 'wrote %s ' % pdfPath + + +class ShapesTestCase(unittest.TestCase): + "Test generating all kinds of shapes." + + def setUp(self): + "Prepare some things before the tests start." + + self.funcNames = getAllFunctionDrawingNames() + self.drawings = [] + + + def tearDown(self): + "Do what has to be done after the tests are over." + + writePDF(self.drawings) + + + # This should always succeed. If each drawing would be + # wrapped in a dedicated test method like this one, it + # would be possible to have a count for wrong tests + # as well... Something like this is left for later... + def testAllDrawings(self): + "Make a list of drawings." + + for f in self.funcNames: + if f[0:10] == 'getDrawing': + # Make an instance and get its doc string. + # If that fails, use a default error drawing. + _evalFuncDrawing(f,self.drawings) + + +def makeSuite(): + "Make a test suite for unit testing." + + suite = unittest.TestSuite() + suite.addTest(ShapesTestCase('testAllDrawings')) + return suite + + +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) diff --git a/bin/reportlab/graphics/widgetbase.py b/bin/reportlab/graphics/widgetbase.py new file mode 100644 index 00000000000..c529c0ded88 --- /dev/null +++ b/bin/reportlab/graphics/widgetbase.py @@ -0,0 +1,502 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/widgetbase.py +__version__=''' $Id: widgetbase.py 2668 2005-09-05 10:23:51Z rgbecker $ ''' +import string + +from reportlab.graphics import shapes +from reportlab import rl_config +from reportlab.lib import colors +from reportlab.lib.validators import * +from reportlab.lib.attrmap import * + + +class PropHolder: + '''Base for property holders''' + + _attrMap = None + + def verify(self): + """If the _attrMap attribute is not None, this + checks all expected attributes are present; no + unwanted attributes are present; and (if a + checking function is found) checks each + attribute has a valid value. Either succeeds + or raises an informative exception. + """ + + if self._attrMap is not None: + for key in self.__dict__.keys(): + if key[0] <> '_': + msg = "Unexpected attribute %s found in %s" % (key, self) + assert self._attrMap.has_key(key), msg + for (attr, metavalue) in self._attrMap.items(): + msg = "Missing attribute %s from %s" % (attr, self) + assert hasattr(self, attr), msg + value = getattr(self, attr) + args = (value, attr, self.__class__.__name__) + assert metavalue.validate(value), "Invalid value %s for attribute %s in class %s" % args + + if rl_config.shapeChecking: + """This adds the ability to check every attribute assignment + as it is made. It slows down shapes but is a big help when + developing. It does not get defined if rl_config.shapeChecking = 0. + """ + + def __setattr__(self, name, value): + """By default we verify. This could be off + in some parallel base classes.""" + validateSetattr(self,name,value) + + + def getProperties(self,recur=1): + """Returns a list of all properties which can be edited and + which are not marked as private. This may include 'child + widgets' or 'primitive shapes'. You are free to override + this and provide alternative implementations; the default + one simply returns everything without a leading underscore. + """ + + from reportlab.lib.validators import isValidChild + + # TODO when we need it, but not before - + # expose sequence contents? + + props = {} + for name in self.__dict__.keys(): + if name[0:1] <> '_': + component = getattr(self, name) + + if recur and isValidChild(component): + # child object, get its properties too + childProps = component.getProperties(recur=recur) + for (childKey, childValue) in childProps.items(): + #key might be something indexed like '[2].fillColor' + #or simple like 'fillColor'; in the former case we + #don't need a '.' between me and my child. + if childKey[0] == '[': + props['%s%s' % (name, childKey)] = childValue + else: + props['%s.%s' % (name, childKey)] = childValue + else: + props[name] = component + + return props + + + def setProperties(self, propDict): + """Permits bulk setting of properties. These may include + child objects e.g. "chart.legend.width = 200". + + All assignments will be validated by the object as if they + were set individually in python code. + + All properties of a top-level object are guaranteed to be + set before any of the children, which may be helpful to + widget designers. + """ + + childPropDicts = {} + for (name, value) in propDict.items(): + parts = string.split(name, '.', 1) + if len(parts) == 1: + #simple attribute, set it now + setattr(self, name, value) + else: + (childName, remains) = parts + try: + childPropDicts[childName][remains] = value + except KeyError: + childPropDicts[childName] = {remains: value} + + # now assign to children + for (childName, childPropDict) in childPropDicts.items(): + child = getattr(self, childName) + child.setProperties(childPropDict) + + + def dumpProperties(self, prefix=""): + """Convenience. Lists them on standard output. You + may provide a prefix - mostly helps to generate code + samples for documentation. + """ + + propList = self.getProperties().items() + propList.sort() + if prefix: + prefix = prefix + '.' + for (name, value) in propList: + print '%s%s = %s' % (prefix, name, value) + + +class Widget(PropHolder, shapes.UserNode): + """Base for all user-defined widgets. Keep as simple as possible. Does + not inherit from Shape so that we can rewrite shapes without breaking + widgets and vice versa.""" + + def _setKeywords(self,**kw): + for k,v in kw.items(): + if not self.__dict__.has_key(k): + setattr(self,k,v) + + def draw(self): + msg = "draw() must be implemented for each Widget!" + raise shapes.NotImplementedError, msg + + def demo(self): + msg = "demo() must be implemented for each Widget!" + raise shapes.NotImplementedError, msg + + def provideNode(self): + return self.draw() + + def getBounds(self): + "Return outer boundary as x1,y1,x2,y2. Can be overridden for efficiency" + return self.draw().getBounds() + +_ItemWrapper={} + +class TypedPropertyCollection(PropHolder): + """A container with properties for objects of the same kind. + + This makes it easy to create lists of objects. You initialize + it with a class of what it is to contain, and that is all you + can add to it. You can assign properties to the collection + as a whole, or to a numeric index within it; if so it creates + a new child object to hold that data. + + So: + wedges = TypedPropertyCollection(WedgeProperties) + wedges.strokeWidth = 2 # applies to all + wedges.strokeColor = colors.red # applies to all + wedges[3].strokeColor = colors.blue # only to one + + The last line should be taken as a prescription of how to + create wedge no. 3 if one is needed; no error is raised if + there are only two data points. + + We try and make sensible use of tuple indeces. + line[(3,x)] is backed by line[(3,)], line[3] & line + """ + + def __init__(self, exampleClass): + #give it same validation rules as what it holds + self.__dict__['_value'] = exampleClass() + self.__dict__['_children'] = {} + + def wKlassFactory(self,Klass): + class WKlass(Klass): + def __getattr__(self,name): + try: + return self.__class__.__bases__[0].__getattr__(self,name) + except: + i = self._index + if i: + c = self._parent._children + if c.has_key(i) and c[i].__dict__.has_key(name): + return getattr(c[i],name) + elif len(i)==1: + i = i[0] + if c.has_key(i) and c[i].__dict__.has_key(name): + return getattr(c[i],name) + return getattr(self._parent,name) + return WKlass + + def __getitem__(self, index): + try: + return self._children[index] + except KeyError: + Klass = self._value.__class__ + if _ItemWrapper.has_key(Klass): + WKlass = _ItemWrapper[Klass] + else: + _ItemWrapper[Klass] = WKlass = self.wKlassFactory(Klass) + + child = WKlass() + child._parent = self + if type(index) in (type(()),type([])): + index = tuple(index) + if len(index)>1: + child._index = tuple(index[:-1]) + else: + child._index = None + else: + child._index = None + for i in filter(lambda x,K=child.__dict__.keys(): x in K,child._attrMap.keys()): + del child.__dict__[i] + + self._children[index] = child + return child + + def has_key(self,key): + if type(key) in (type(()),type([])): key = tuple(key) + return self._children.has_key(key) + + def __setitem__(self, key, value): + msg = "This collection can only hold objects of type %s" % self._value.__class__.__name__ + assert isinstance(value, self._value.__class__), msg + + def __len__(self): + return len(self._children.keys()) + + def getProperties(self,recur=1): + # return any children which are defined and whatever + # differs from the parent + props = {} + + for (key, value) in self._value.getProperties(recur=recur).items(): + props['%s' % key] = value + + for idx in self._children.keys(): + childProps = self._children[idx].getProperties(recur=recur) + for (key, value) in childProps.items(): + if not hasattr(self,key) or getattr(self, key)<>value: + newKey = '[%s].%s' % (idx, key) + props[newKey] = value + return props + + def setVector(self,**kw): + for name, value in kw.items(): + for i in xrange(len(value)): + setattr(self[i],name,value[i]) + + def __getattr__(self,name): + return getattr(self._value,name) + + def __setattr__(self,name,value): + return setattr(self._value,name,value) + +## No longer needed! +class StyleProperties(PropHolder): + """A container class for attributes used in charts and legends. + + Attributes contained can be those for any graphical element + (shape?) in the ReportLab graphics package. The idea for this + container class is to be useful in combination with legends + and/or the individual appearance of data series in charts. + + A legend could be as simple as a wrapper around a list of style + properties, where the 'desc' attribute contains a descriptive + string and the rest could be used by the legend e.g. to draw + something like a color swatch. The graphical presentation of + the legend would be its own business, though. + + A chart could be inspecting a legend or, more directly, a list + of style properties to pick individual attributes that it knows + about in order to render a particular row of the data. A bar + chart e.g. could simply use 'strokeColor' and 'fillColor' for + drawing the bars while a line chart could also use additional + ones like strokeWidth. + """ + + _attrMap = AttrMap( + strokeWidth = AttrMapValue(isNumber), + strokeLineCap = AttrMapValue(isNumber), + strokeLineJoin = AttrMapValue(isNumber), + strokeMiterLimit = AttrMapValue(None), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone), + strokeOpacity = AttrMapValue(isNumber), + strokeColor = AttrMapValue(isColorOrNone), + fillColor = AttrMapValue(isColorOrNone), + desc = AttrMapValue(isString), + ) + + def __init__(self, **kwargs): + "Initialize with attributes if any." + + for k, v in kwargs.items(): + setattr(self, k, v) + + + def __setattr__(self, name, value): + "Verify attribute name and value, before setting it." + validateSetattr(self,name,value) + + +class TwoCircles(Widget): + def __init__(self): + self.leftCircle = shapes.Circle(100,100,20, fillColor=colors.red) + self.rightCircle = shapes.Circle(300,100,20, fillColor=colors.red) + + def draw(self): + return shapes.Group(self.leftCircle, self.rightCircle) + + +class Face(Widget): + """This draws a face with two eyes. + + It exposes a couple of properties + to configure itself and hides all other details. + """ + + _attrMap = AttrMap( + x = AttrMapValue(isNumber), + y = AttrMapValue(isNumber), + size = AttrMapValue(isNumber), + skinColor = AttrMapValue(isColorOrNone), + eyeColor = AttrMapValue(isColorOrNone), + mood = AttrMapValue(OneOf('happy','sad','ok')), + ) + + def __init__(self): + self.x = 10 + self.y = 10 + self.size = 80 + self.skinColor = None + self.eyeColor = colors.blue + self.mood = 'happy' + + def demo(self): + pass + + def draw(self): + s = self.size # abbreviate as we will use this a lot + g = shapes.Group() + g.transform = [1,0,0,1,self.x, self.y] + + # background + g.add(shapes.Circle(s * 0.5, s * 0.5, s * 0.5, fillColor=self.skinColor)) + + # left eye + g.add(shapes.Circle(s * 0.35, s * 0.65, s * 0.1, fillColor=colors.white)) + g.add(shapes.Circle(s * 0.35, s * 0.65, s * 0.05, fillColor=self.eyeColor)) + + # right eye + g.add(shapes.Circle(s * 0.65, s * 0.65, s * 0.1, fillColor=colors.white)) + g.add(shapes.Circle(s * 0.65, s * 0.65, s * 0.05, fillColor=self.eyeColor)) + + # nose + g.add(shapes.Polygon( + points=[s * 0.5, s * 0.6, s * 0.4, s * 0.3, s * 0.6, s * 0.3], + fillColor=None)) + + # mouth + if self.mood == 'happy': + offset = -0.05 + elif self.mood == 'sad': + offset = +0.05 + else: + offset = 0 + + g.add(shapes.Polygon( + points = [ + s * 0.3, s * 0.2, #left of mouth + s * 0.7, s * 0.2, #right of mouth + s * 0.6, s * (0.2 + offset), # the bit going up or down + s * 0.4, s * (0.2 + offset) # the bit going up or down + ], + fillColor = colors.pink, + strokeColor = colors.red, + strokeWidth = s * 0.03 + )) + + return g + + +class TwoFaces(Widget): + def __init__(self): + self.faceOne = Face() + self.faceOne.mood = "happy" + self.faceTwo = Face() + self.faceTwo.x = 100 + self.faceTwo.mood = "sad" + + def draw(self): + """Just return a group""" + return shapes.Group(self.faceOne, self.faceTwo) + + def demo(self): + """The default case already looks good enough, + no implementation needed here""" + pass + +class Sizer(Widget): + "Container to show size of all enclosed objects" + + _attrMap = AttrMap(BASE=shapes.SolidShape, + contents = AttrMapValue(isListOfShapes,desc="Contained drawable elements"), + ) + def __init__(self, *elements): + self.contents = [] + self.fillColor = colors.cyan + self.strokeColor = colors.magenta + + for elem in elements: + self.add(elem) + + def _addNamedNode(self,name,node): + 'if name is not None add an attribute pointing to node and add to the attrMap' + if name: + if name not in self._attrMap.keys(): + self._attrMap[name] = AttrMapValue(isValidChild) + setattr(self, name, node) + + def add(self, node, name=None): + """Appends non-None child node to the 'contents' attribute. In addition, + if a name is provided, it is subsequently accessible by name + """ + # propagates properties down + if node is not None: + assert isValidChild(node), "Can only add Shape or UserNode objects to a Group" + self.contents.append(node) + self._addNamedNode(name,node) + + def getBounds(self): + # get bounds of each object + if self.contents: + b = [] + for elem in self.contents: + b.append(elem.getBounds()) + return shapes.getRectsBounds(b) + else: + return (0,0,0,0) + + def draw(self): + g = shapes.Group() + (x1, y1, x2, y2) = self.getBounds() + r = shapes.Rect( + x = x1, + y = y1, + width = x2-x1, + height = y2-y1, + fillColor = self.fillColor, + strokeColor = self.strokeColor + ) + g.add(r) + for elem in self.contents: + g.add(elem) + return g + +def test(): + from reportlab.graphics.charts.piecharts import WedgeProperties + wedges = TypedPropertyCollection(WedgeProperties) + wedges.fillColor = colors.red + wedges.setVector(fillColor=(colors.blue,colors.green,colors.white)) + print len(_ItemWrapper) + + d = shapes.Drawing(400, 200) + tc = TwoCircles() + d.add(tc) + import renderPDF + renderPDF.drawToFile(d, 'sample_widget.pdf', 'A Sample Widget') + print 'saved sample_widget.pdf' + + d = shapes.Drawing(400, 200) + f = Face() + f.skinColor = colors.yellow + f.mood = "sad" + d.add(f, name='theFace') + print 'drawing 1 properties:' + d.dumpProperties() + renderPDF.drawToFile(d, 'face.pdf', 'A Sample Widget') + print 'saved face.pdf' + + d2 = d.expandUserNodes() + renderPDF.drawToFile(d2, 'face_copy.pdf', 'An expanded drawing') + print 'saved face_copy.pdf' + print 'drawing 2 properties:' + d2.dumpProperties() + + +if __name__=='__main__': + test() diff --git a/bin/reportlab/graphics/widgets/__init__.py b/bin/reportlab/graphics/widgets/__init__.py new file mode 100644 index 00000000000..8558748139b --- /dev/null +++ b/bin/reportlab/graphics/widgets/__init__.py @@ -0,0 +1,4 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/widgets/__init__.py +__version__=''' $Id: __init__.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' \ No newline at end of file diff --git a/bin/reportlab/graphics/widgets/eventcal.py b/bin/reportlab/graphics/widgets/eventcal.py new file mode 100644 index 00000000000..20f0b817431 --- /dev/null +++ b/bin/reportlab/graphics/widgets/eventcal.py @@ -0,0 +1,303 @@ +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/widgets/eventcal.py +# Event Calendar widget +# author: Andy Robinson +"""This file is a +""" +__version__=''' $Id: eventcal.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' + +from reportlab.lib import colors +from reportlab.lib.validators import * +from reportlab.lib.attrmap import * +from reportlab.graphics.shapes import Line, Rect, Polygon, Drawing, Group, String, Circle, Wedge +from reportlab.graphics.charts.textlabels import Label +from reportlab.graphics.widgetbase import Widget +from reportlab.graphics import renderPDF + + + + +class EventCalendar(Widget): + def __init__(self): + self.x = 0 + self.y = 0 + self.width = 300 + self.height = 150 + self.timeColWidth = None # if declared, use it; otherwise auto-size. + self.trackRowHeight = 20 + self.data = [] # list of Event objects + self.trackNames = None + + self.startTime = None #displays ALL data on day if not set + self.endTime = None # displays ALL data on day if not set + self.day = 0 + + + # we will keep any internal geometry variables + # here. These are computed by computeSize(), + # which is the first thing done when drawing. + self._talksVisible = [] # subset of data which will get plotted, cache + self._startTime = None + self._endTime = None + self._trackCount = 0 + self._colWidths = [] + self._colLeftEdges = [] # left edge of each column + + def computeSize(self): + "Called at start of draw. Sets various column widths" + self._talksVisible = self.getRelevantTalks(self.data) + self._trackCount = len(self.getAllTracks()) + self.computeStartAndEndTimes() + self._colLeftEdges = [self.x] + if self.timeColWidth is None: + w = self.width / (1 + self._trackCount) + self._colWidths = [w] * (1+ self._trackCount) + for i in range(self._trackCount): + self._colLeftEdges.append(self._colLeftEdges[-1] + w) + else: + self._colWidths = [self.timeColWidth] + w = (self.width - self.timeColWidth) / self._trackCount + for i in range(self._trackCount): + self._colWidths.append(w) + self._colLeftEdges.append(self._colLeftEdges[-1] + w) + + + + def computeStartAndEndTimes(self): + "Work out first and last times to display" + if self.startTime: + self._startTime = self.startTime + else: + for (title, speaker, trackId, day, start, duration) in self._talksVisible: + + if self._startTime is None: #first one + self._startTime = start + else: + if start < self._startTime: + self._startTime = start + + if self.endTime: + self._endTime = self.endTime + else: + for (title, speaker, trackId, day, start, duration) in self._talksVisible: + if self._endTime is None: #first one + self._endTime = start + duration + else: + if start + duration > self._endTime: + self._endTime = start + duration + + + + + def getAllTracks(self): + tracks = [] + for (title, speaker, trackId, day, hours, duration) in self.data: + if trackId is not None: + if trackId not in tracks: + tracks.append(trackId) + tracks.sort() + return tracks + + def getRelevantTalks(self, talkList): + "Scans for tracks actually used" + used = [] + for talk in talkList: + (title, speaker, trackId, day, hours, duration) = talk + assert trackId <> 0, "trackId must be None or 1,2,3... zero not allowed!" + if day == self.day: + if (((self.startTime is None) or ((hours + duration) >= self.startTime)) + and ((self.endTime is None) or (hours <= self.endTime))): + used.append(talk) + return used + + def scaleTime(self, theTime): + "Return y-value corresponding to times given" + axisHeight = self.height - self.trackRowHeight + # compute fraction between 0 and 1, 0 is at start of period + proportionUp = ((theTime - self._startTime) / (self._endTime - self._startTime)) + y = self.y + axisHeight - (axisHeight * proportionUp) + return y + + + def getTalkRect(self, startTime, duration, trackId, text): + "Return shapes for a specific talk" + g = Group() + y_bottom = self.scaleTime(startTime + duration) + y_top = self.scaleTime(startTime) + y_height = y_top - y_bottom + + if trackId is None: + #spans all columns + x = self._colLeftEdges[1] + width = self.width - self._colWidths[0] + else: + #trackId is 1-based and these arrays have the margin info in column + #zero, so no need to add 1 + x = self._colLeftEdges[trackId] + width = self._colWidths[trackId] + + lab = Label() + lab.setText(text) + lab.setOrigin(x + 0.5*width, y_bottom+0.5*y_height) + lab.boxAnchor = 'c' + lab.width = width + lab.height = y_height + lab.fontSize = 6 + + r = Rect(x, y_bottom, width, y_height, fillColor=colors.cyan) + g.add(r) + g.add(lab) + + #now for a label + # would expect to color-code and add text + return g + + def draw(self): + self.computeSize() + g = Group() + + # time column + g.add(Rect(self.x, self.y, self._colWidths[0], self.height - self.trackRowHeight, fillColor=colors.cornsilk)) + + # track headers + x = self.x + self._colWidths[0] + y = self.y + self.height - self.trackRowHeight + for trk in range(self._trackCount): + wid = self._colWidths[trk+1] + r = Rect(x, y, wid, self.trackRowHeight, fillColor=colors.yellow) + s = String(x + 0.5*wid, y, 'Track %d' % trk, align='middle') + g.add(r) + g.add(s) + x = x + wid + + for talk in self._talksVisible: + (title, speaker, trackId, day, start, duration) = talk + r = self.getTalkRect(start, duration, trackId, title + '\n' + speaker) + g.add(r) + + + return g + + + + +def test(): + "Make a conference event for day 1 of UP Python 2003" + + + d = Drawing(400,200) + + cal = EventCalendar() + cal.x = 50 + cal.y = 25 + cal.data = [ + # these might be better as objects instead of tuples, since I + # predict a large number of "optionsl" variables to affect + # formatting in future. + + #title, speaker, track id, day, start time (hrs), duration (hrs) + # track ID is 1-based not zero-based! + ('Keynote: Why design another programming language?', 'Guido van Rossum', None, 1, 9.0, 1.0), + + ('Siena Web Service Architecture', 'Marc-Andre Lemburg', 1, 1, 10.5, 1.5), + ('Extreme Programming in Python', 'Chris Withers', 2, 1, 10.5, 1.5), + ('Pattern Experiences in C++', 'Mark Radford', 3, 1, 10.5, 1.5), + ('What is the Type of std::toupper()', 'Gabriel Dos Reis', 4, 1, 10.5, 1.5), + ('Linguistic Variables: Clear Thinking with Fuzzy Logic ', 'Walter Banks', 5, 1, 10.5, 1.5), + + ('lunch, short presentations, vendor presentations', '', None, 1, 12.0, 2.0), + + ("CORBA? Isn't that obsolete", 'Duncan Grisby', 1, 1, 14.0, 1.5), + ("Python Design Patterns", 'Duncan Booth', 2, 1, 14.0, 1.5), + ("Inside Security Checks and Safe Exceptions", 'Brandon Bray', 3, 1, 14.0, 1.5), + ("Studying at a Distance", 'Panel Discussion, Panel to include Alan Lenton & Francis Glassborow', 4, 1, 14.0, 1.5), + ("Coding Standards - Given the ANSI C Standard why do I still need a coding Standard", 'Randy Marques', 5, 1, 14.0, 1.5), + + ("RESTful Python", 'Hamish Lawson', 1, 1, 16.0, 1.5), + ("Parsing made easier - a radical old idea", 'Andrew Koenig', 2, 1, 16.0, 1.5), + ("C++ & Multimethods", 'Julian Smith', 3, 1, 16.0, 1.5), + ("C++ Threading", 'Kevlin Henney', 4, 1, 16.0, 1.5), + ("The Organisation Strikes Back", 'Alan Griffiths & Sarah Lees', 5, 1, 16.0, 1.5), + + ('Birds of a Feather meeting', '', None, 1, 17.5, 2.0), + + ('Keynote: In the Spirit of C', 'Greg Colvin', None, 2, 9.0, 1.0), + + ('The Infinite Filing Cabinet - object storage in Python', 'Jacob Hallen', 1, 2, 10.5, 1.5), + ('Introduction to Python and Jython for C++ and Java Programmers', 'Alex Martelli', 2, 2, 10.5, 1.5), + ('Template metaprogramming in Haskell', 'Simon Peyton Jones', 3, 2, 10.5, 1.5), + ('Plenty People Programming: C++ Programming in a Group, Workshop with a difference', 'Nico Josuttis', 4, 2, 10.5, 1.5), + ('Design and Implementation of the Boost Graph Library', 'Jeremy Siek', 5, 2, 10.5, 1.5), + + ('lunch, short presentations, vendor presentations', '', None, 2, 12.0, 2.0), + + ("Building GUI Applications with PythonCard and PyCrust", 'Andy Todd', 1, 2, 14.0, 1.5), + ("Integrating Python, C and C++", 'Duncan Booth', 2, 2, 14.0, 1.5), + ("Secrets and Pitfalls of Templates", 'Nicolai Josuttis & David Vandevoorde', 3, 2, 14.0, 1.5), + ("Being a Mentor", 'Panel Discussion, Panel to include Alan Lenton & Francis Glassborow', 4, 2, 14.0, 1.5), + ("The Embedded C Extensions to C", 'Willem Wakker', 5, 2, 14.0, 1.5), + + ("Lightning Talks", 'Paul Brian', 1, 2, 16.0, 1.5), + ("Scripting Java Applications with Jython", 'Anthony Eden', 2, 2, 16.0, 1.5), + ("Metaprogramming and the Boost Metaprogramming Library", 'David Abrahams', 3, 2, 16.0, 1.5), + ("A Common Vendor ABI for C++ -- GCC's why, what and not", 'Nathan Sidwell & Gabriel Dos Reis', 4, 2, 16.0, 1.5), + ("The Timing and Cost of Choices", 'Hubert Matthews', 5, 2, 16.0, 1.5), + + ('Birds of a Feather meeting', '', None, 2, 17.5, 2.0), + + ('Keynote: The Cost of C & C++ Compatibility', 'Andy Koenig', None, 3, 9.0, 1.0), + + ('Prying Eyes: Generic Observer Implementations in C++', 'Andrei Alexandrescu', 1, 2, 10.5, 1.5), + ('The Roadmap to Generative Programming With C++', 'Ulrich Eisenecker', 2, 2, 10.5, 1.5), + ('Design Patterns in C++ and C# for the Common Language Runtime', 'Brandon Bray', 3, 2, 10.5, 1.5), + ('Extreme Hour (XH): (workshop) - Jutta Eckstein and Nico Josuttis', 'Jutta Ecstein', 4, 2, 10.5, 1.5), + ('The Lambda Library : Unnamed Functions for C++', 'Jaako Jarvi', 5, 2, 10.5, 1.5), + + ('lunch, short presentations, vendor presentations', '', None, 3, 12.0, 2.0), + + ('Reflective Metaprogramming', 'Daveed Vandevoorde', 1, 3, 14.0, 1.5), + ('Advanced Template Issues and Solutions (double session)', 'Herb Sutter',2, 3, 14.0, 3), + ('Concurrent Programming in Java (double session)', 'Angelika Langer', 3, 3, 14.0, 3), + ('What can MISRA-C (2nd Edition) do for us?', 'Chris Hills', 4, 3, 14.0, 1.5), + ('C++ Metaprogramming Concepts and Results', 'Walter E Brown', 5, 3, 14.0, 1.5), + + ('Binding C++ to Python with the Boost Python Library', 'David Abrahams', 1, 3, 16.0, 1.5), + ('Using Aspect Oriented Programming for Enterprise Application Integration', 'Arno Schmidmeier', 4, 3, 16.0, 1.5), + ('Defective C++', 'Marc Paterno', 5, 3, 16.0, 1.5), + + ("Speakers' Banquet & Birds of a Feather meeting", '', None, 3, 17.5, 2.0), + + ('Keynote: The Internet, Software and Computers - A Report Card', 'Alan Lenton', None, 4, 9.0, 1.0), + + ('Multi-Platform Software Development; Lessons from the Boost libraries', 'Beman Dawes', 1, 5, 10.5, 1.5), + ('The Stability of the C++ ABI', 'Steve Clamage', 2, 5, 10.5, 1.5), + ('Generic Build Support - A Pragmatic Approach to the Software Build Process', 'Randy Marques', 3, 5, 10.5, 1.5), + ('How to Handle Project Managers: a survival guide', 'Barb Byro', 4, 5, 10.5, 1.5), + + ('lunch, ACCU AGM', '', None, 5, 12.0, 2.0), + + ('Sauce: An OO recursive descent parser; its design and implementation.', 'Jon Jagger', 1, 5, 14.0, 1.5), + ('GNIRTS ESAC REWOL - Bringing the UNIX filters to the C++ iostream library.', 'JC van Winkel', 2, 5, 14.0, 1.5), + ('Pattern Writing: Live and Direct', 'Frank Buschmann & Kevlin Henney', 3, 5, 14.0, 3.0), + ('The Future of Programming Languages - A Goldfish Bowl', 'Francis Glassborow and friends', 3, 5, 14.0, 1.5), + + ('Honey, I Shrunk the Threads: Compile-time checked multithreaded transactions in C++', 'Andrei Alexandrescu', 1, 5, 16.0, 1.5), + ('Fun and Functionality with Functors', 'Lois Goldthwaite', 2, 5, 16.0, 1.5), + ('Agile Enough?', 'Alan Griffiths', 4, 5, 16.0, 1.5), + ("Conference Closure: A brief plenary session", '', None, 5, 17.5, 0.5), + + ] + + #return cal + cal.day = 1 + + d.add(cal) + + + for format in ['pdf']:#,'gif','png']: + out = d.asString(format) + open('eventcal.%s' % format, 'wb').write(out) + print 'saved eventcal.%s' % format + +if __name__=='__main__': + test() diff --git a/bin/reportlab/graphics/widgets/flags.py b/bin/reportlab/graphics/widgets/flags.py new file mode 100644 index 00000000000..d83541c0404 --- /dev/null +++ b/bin/reportlab/graphics/widgets/flags.py @@ -0,0 +1,879 @@ +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/widgets/flags.py +# Flag Widgets - a collection of flags as widgets +# author: John Precedo (johnp@reportlab.com) +"""This file is a collection of flag graphics as widgets. + +All flags are represented at the ratio of 1:2, even where the official ratio for the flag is something else +(such as 3:5 for the German national flag). The only exceptions are for where this would look _very_ wrong, +such as the Danish flag whose (ratio is 28:37), or the Swiss flag (which is square). + +Unless otherwise stated, these flags are all the 'national flags' of the countries, rather than their +state flags, naval flags, ensigns or any other variants. (National flags are the flag flown by civilians +of a country and the ones usually used to represent a country abroad. State flags are the variants used by +the government and by diplomatic missions overseas). + +To check on how close these are to the 'official' representations of flags, check the World Flag Database at +http://www.flags.ndirect.co.uk/ + +The flags this file contains are: + +EU Members: +United Kingdom, Austria, Belgium, Denmark, Finland, France, Germany, Greece, Ireland, Italy, Luxembourg, +Holland (The Netherlands), Spain, Sweden + +Others: +USA, Czech Republic, European Union, Switzerland, Turkey, Brazil + +(Brazilian flag contributed by Publio da Costa Melo [publio@planetarium.com.br]). +""" +__version__=''' $Id: flags.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' + +from reportlab.lib import colors +from reportlab.lib.validators import * +from reportlab.lib.attrmap import * +from reportlab.graphics.shapes import Line, Rect, Polygon, Drawing, Group, String, Circle, Wedge +from reportlab.graphics.widgetbase import Widget +from reportlab.graphics import renderPDF +from signsandsymbols import _Symbol +import copy +from math import sin, cos, pi + +validFlag=OneOf(None, + 'UK', + 'USA', + 'Afghanistan', + 'Austria', + 'Belgium', + 'China', + 'Cuba', + 'Denmark', + 'Finland', + 'France', + 'Germany', + 'Greece', + 'Ireland', + 'Italy', + 'Japan', + 'Luxembourg', + 'Holland', + 'Palestine', + 'Portugal', + 'Russia', + 'Spain', + 'Sweden', + 'Norway', + 'CzechRepublic', + 'Turkey', + 'Switzerland', + 'EU', + 'Brazil' + ) + +_size = 100. + +class Star(_Symbol): + """This draws a 5-pointed star. + + possible attributes: + 'x', 'y', 'size', 'fillColor', 'strokeColor' + + """ + _attrMap = AttrMap(BASE=_Symbol, + angle = AttrMapValue(isNumber, desc='angle in degrees'), + ) + _size = 100. + + def __init__(self): + _Symbol.__init__(self) + self.size = 100 + self.fillColor = colors.yellow + self.strokeColor = None + self.angle = 0 + + def demo(self): + D = Drawing(200, 100) + et = Star() + et.x=50 + et.y=0 + D.add(et) + labelFontSize = 10 + D.add(String(et.x+(et.size/2.0),(et.y-(1.2*labelFontSize)), + et.__class__.__name__, fillColor=colors.black, textAnchor='middle', + fontSize=labelFontSize)) + return D + + def draw(self): + s = float(self.size) #abbreviate as we will use this a lot + g = Group() + + # new algorithm from markers.StarFive + R = float(self.size)/2 + r = R*sin(18*(pi/180.0))/cos(36*(pi/180.0)) + P = [] + angle = 90 + for i in xrange(5): + for radius in R, r: + theta = angle*(pi/180.0) + P.append(radius*cos(theta)) + P.append(radius*sin(theta)) + angle = angle + 36 + # star specific bits + star = Polygon(P, + fillColor = self.fillColor, + strokeColor = self.strokeColor, + strokeWidth=s/50) + g.rotate(self.angle) + g.shift(self.x+self.dx,self.y+self.dy) + g.add(star) + + return g + +class Flag(_Symbol): + """This is a generic flag class that all the flags in this file use as a basis. + + This class basically provides edges and a tidy-up routine to hide any bits of + line that overlap the 'outside' of the flag + + possible attributes: + 'x', 'y', 'size', 'fillColor' + """ + + _attrMap = AttrMap(BASE=_Symbol, + fillColor = AttrMapValue(isColor, desc='Background color'), + border = AttrMapValue(isBoolean, 'Whether a background is drawn'), + kind = AttrMapValue(validFlag, desc='Which flag'), + ) + + _cache = {} + + def __init__(self,**kw): + _Symbol.__init__(self) + self.kind = None + self.size = 100 + self.fillColor = colors.white + self.border=1 + self.setProperties(kw) + + def availableFlagNames(self): + '''return a list of the things we can display''' + return filter(lambda x: x is not None, self._attrMap['kind'].validate._enum) + + def _Flag_None(self): + s = _size # abbreviate as we will use this a lot + g = Group() + g.add(Rect(0, 0, s*2, s, fillColor = colors.purple, strokeColor = colors.black, strokeWidth=0)) + return g + + def _borderDraw(self,f): + s = self.size # abbreviate as we will use this a lot + g = Group() + g.add(f) + x, y, sW = self.x+self.dx, self.y+self.dy, self.strokeWidth/2. + g.insert(0,Rect(-sW, -sW, width=getattr(self,'_width',2*s)+3*sW, height=getattr(self,'_height',s)+2*sW, + fillColor = None, strokeColor = self.strokeColor, strokeWidth=sW*2)) + g.shift(x,y) + g.scale(s/_size, s/_size) + return g + + def draw(self): + kind = self.kind or 'None' + f = self._cache.get(kind) + if not f: + f = getattr(self,'_Flag_'+kind)() + self._cache[kind] = f._explode() + return self._borderDraw(f) + + def clone(self): + return copy.copy(self) + + def demo(self): + D = Drawing(200, 100) + name = self.availableFlagNames() + import time + name = name[int(time.time()) % len(name)] + fx = Flag() + fx.kind = name + fx.x = 0 + fx.y = 0 + D.add(fx) + labelFontSize = 10 + D.add(String(fx.x+(fx.size/2),(fx.y-(1.2*labelFontSize)), + name, fillColor=colors.black, textAnchor='middle', + fontSize=labelFontSize)) + labelFontSize = int(fx.size/4) + D.add(String(fx.x+(fx.size),(fx.y+((fx.size/2))), + "SAMPLE", fillColor=colors.gold, textAnchor='middle', + fontSize=labelFontSize, fontName="Helvetica-Bold")) + return D + + def _Flag_UK(self): + s = _size + g = Group() + w = s*2 + g.add(Rect(0, 0, w, s, fillColor = colors.navy, strokeColor = colors.black, strokeWidth=0)) + g.add(Polygon([0,0, s*.225,0, w,s*(1-.1125), w,s, w-s*.225,s, 0, s*.1125], fillColor = colors.mintcream, strokeColor=None, strokeWidth=0)) + g.add(Polygon([0,s*(1-.1125), 0, s, s*.225,s, w, s*.1125, w,0, w-s*.225,0], fillColor = colors.mintcream, strokeColor=None, strokeWidth=0)) + g.add(Polygon([0, s-(s/15), (s-((s/10)*4)), (s*0.65), (s-(s/10)*3), (s*0.65), 0, s], fillColor = colors.red, strokeColor = None, strokeWidth=0)) + g.add(Polygon([0, 0, (s-((s/10)*3)), (s*0.35), (s-((s/10)*2)), (s*0.35), (s/10), 0], fillColor = colors.red, strokeColor = None, strokeWidth=0)) + g.add(Polygon([w, s, (s+((s/10)*3)), (s*0.65), (s+((s/10)*2)), (s*0.65), w-(s/10), s], fillColor = colors.red, strokeColor = None, strokeWidth=0)) + g.add(Polygon([w, (s/15), (s+((s/10)*4)), (s*0.35), (s+((s/10)*3)), (s*0.35), w, 0], fillColor = colors.red, strokeColor = None, strokeWidth=0)) + g.add(Rect(((s*0.42)*2), 0, width=(0.16*s)*2, height=s, fillColor = colors.mintcream, strokeColor = None, strokeWidth=0)) + g.add(Rect(0, (s*0.35), width=w, height=s*0.3, fillColor = colors.mintcream, strokeColor = None, strokeWidth=0)) + g.add(Rect(((s*0.45)*2), 0, width=(0.1*s)*2, height=s, fillColor = colors.red, strokeColor = None, strokeWidth=0)) + g.add(Rect(0, (s*0.4), width=w, height=s*0.2, fillColor = colors.red, strokeColor = None, strokeWidth=0)) + return g + + def _Flag_USA(self): + s = _size # abbreviate as we will use this a lot + g = Group() + + box = Rect(0, 0, s*2, s, fillColor = colors.mintcream, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + for stripecounter in range (13,0, -1): + stripeheight = s/13.0 + if not (stripecounter%2 == 0): + stripecolor = colors.red + else: + stripecolor = colors.mintcream + redorwhiteline = Rect(0, (s-(stripeheight*stripecounter)), width=s*2, height=stripeheight, + fillColor = stripecolor, strokeColor = None, strokeWidth=20) + g.add(redorwhiteline) + + bluebox = Rect(0, (s-(stripeheight*7)), width=0.8*s, height=stripeheight*7, + fillColor = colors.darkblue, strokeColor = None, strokeWidth=0) + g.add(bluebox) + + lss = s*0.045 + lss2 = lss/2 + s9 = s/9 + s7 = s/7 + for starxcounter in range(5): + for starycounter in range(4): + ls = Star() + ls.size = lss + ls.x = 0-s/22+lss/2+s7+starxcounter*s7 + ls.fillColor = colors.mintcream + ls.y = s-(starycounter+1)*s9+lss2 + g.add(ls) + + for starxcounter in range(6): + for starycounter in range(5): + ls = Star() + ls.size = lss + ls.x = 0-(s/22)+lss/2+s/14+starxcounter*s7 + ls.fillColor = colors.mintcream + ls.y = s-(starycounter+1)*s9+(s/18)+lss2 + g.add(ls) + return g + + def _Flag_Afghanistan(self): + s = _size + g = Group() + + box = Rect(0, 0, s*2, s, + fillColor = colors.mintcream, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + greenbox = Rect(0, ((s/3.0)*2.0), width=s*2.0, height=s/3.0, + fillColor = colors.limegreen, strokeColor = None, strokeWidth=0) + g.add(greenbox) + + blackbox = Rect(0, 0, width=s*2.0, height=s/3.0, + fillColor = colors.black, strokeColor = None, strokeWidth=0) + g.add(blackbox) + return g + + def _Flag_Austria(self): + s = _size # abbreviate as we will use this a lot + g = Group() + + box = Rect(0, 0, s*2, s, fillColor = colors.mintcream, + strokeColor = colors.black, strokeWidth=0) + g.add(box) + + + redbox1 = Rect(0, 0, width=s*2.0, height=s/3.0, + fillColor = colors.red, strokeColor = None, strokeWidth=0) + g.add(redbox1) + + redbox2 = Rect(0, ((s/3.0)*2.0), width=s*2.0, height=s/3.0, + fillColor = colors.red, strokeColor = None, strokeWidth=0) + g.add(redbox2) + return g + + def _Flag_Belgium(self): + s = _size + g = Group() + + box = Rect(0, 0, s*2, s, + fillColor = colors.black, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + + box1 = Rect(0, 0, width=(s/3.0)*2.0, height=s, + fillColor = colors.black, strokeColor = None, strokeWidth=0) + g.add(box1) + + box2 = Rect(((s/3.0)*2.0), 0, width=(s/3.0)*2.0, height=s, + fillColor = colors.gold, strokeColor = None, strokeWidth=0) + g.add(box2) + + box3 = Rect(((s/3.0)*4.0), 0, width=(s/3.0)*2.0, height=s, + fillColor = colors.red, strokeColor = None, strokeWidth=0) + g.add(box3) + return g + + def _Flag_China(self): + s = _size + g = Group() + self._width = w = s*1.5 + g.add(Rect(0, 0, w, s, fillColor=colors.red, strokeColor=None, strokeWidth=0)) + + def addStar(x,y,size,angle,g=g,w=s/20,x0=0,y0=s/2): + s = Star() + s.fillColor=colors.yellow + s.angle = angle + s.size = size*w*2 + s.x = x*w+x0 + s.y = y*w+y0 + g.add(s) + + addStar(5,5,3, 0) + addStar(10,1,1,36.86989765) + addStar(12,3,1,8.213210702) + addStar(12,6,1,16.60154960) + addStar(10,8,1,53.13010235) + return g + + def _Flag_Cuba(self): + s = _size + g = Group() + + for i in range(5): + stripe = Rect(0, i*s/5, width=s*2, height=s/5, + fillColor = [colors.darkblue, colors.mintcream][i%2], + strokeColor = None, + strokeWidth=0) + g.add(stripe) + + redwedge = Polygon(points = [ 0, 0, 4*s/5, (s/2), 0, s], + fillColor = colors.red, strokeColor = None, strokeWidth=0) + g.add(redwedge) + + star = Star() + star.x = 2.5*s/10 + star.y = s/2 + star.size = 3*s/10 + star.fillColor = colors.white + g.add(star) + + box = Rect(0, 0, s*2, s, + fillColor = None, + strokeColor = colors.black, + strokeWidth=0) + g.add(box) + + return g + + def _Flag_Denmark(self): + s = _size + g = Group() + self._width = w = s*1.4 + + box = Rect(0, 0, w, s, + fillColor = colors.red, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + whitebox1 = Rect(((s/5)*2), 0, width=s/6, height=s, + fillColor = colors.mintcream, strokeColor = None, strokeWidth=0) + g.add(whitebox1) + + whitebox2 = Rect(0, ((s/2)-(s/12)), width=w, height=s/6, + fillColor = colors.mintcream, strokeColor = None, strokeWidth=0) + g.add(whitebox2) + return g + + def _Flag_Finland(self): + s = _size + g = Group() + + # crossbox specific bits + box = Rect(0, 0, s*2, s, + fillColor = colors.ghostwhite, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + blueline1 = Rect((s*0.6), 0, width=0.3*s, height=s, + fillColor = colors.darkblue, strokeColor = None, strokeWidth=0) + g.add(blueline1) + + blueline2 = Rect(0, (s*0.4), width=s*2, height=s*0.3, + fillColor = colors.darkblue, strokeColor = None, strokeWidth=0) + g.add(blueline2) + return g + + def _Flag_France(self): + s = _size + g = Group() + + box = Rect(0, 0, s*2, s, fillColor = colors.navy, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + bluebox = Rect(0, 0, width=((s/3.0)*2.0), height=s, + fillColor = colors.blue, strokeColor = None, strokeWidth=0) + g.add(bluebox) + + whitebox = Rect(((s/3.0)*2.0), 0, width=((s/3.0)*2.0), height=s, + fillColor = colors.mintcream, strokeColor = None, strokeWidth=0) + g.add(whitebox) + + redbox = Rect(((s/3.0)*4.0), 0, width=((s/3.0)*2.0), height=s, + fillColor = colors.red, + strokeColor = None, + strokeWidth=0) + g.add(redbox) + return g + + def _Flag_Germany(self): + s = _size + g = Group() + + box = Rect(0, 0, s*2, s, + fillColor = colors.gold, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + blackbox1 = Rect(0, ((s/3.0)*2.0), width=s*2.0, height=s/3.0, + fillColor = colors.black, strokeColor = None, strokeWidth=0) + g.add(blackbox1) + + redbox1 = Rect(0, (s/3.0), width=s*2.0, height=s/3.0, + fillColor = colors.orangered, strokeColor = None, strokeWidth=0) + g.add(redbox1) + return g + + def _Flag_Greece(self): + s = _size + g = Group() + + box = Rect(0, 0, s*2, s, fillColor = colors.gold, + strokeColor = colors.black, strokeWidth=0) + g.add(box) + + for stripecounter in range (9,0, -1): + stripeheight = s/9.0 + if not (stripecounter%2 == 0): + stripecolor = colors.deepskyblue + else: + stripecolor = colors.mintcream + + blueorwhiteline = Rect(0, (s-(stripeheight*stripecounter)), width=s*2, height=stripeheight, + fillColor = stripecolor, strokeColor = None, strokeWidth=20) + g.add(blueorwhiteline) + + bluebox1 = Rect(0, ((s)-stripeheight*5), width=(stripeheight*5), height=stripeheight*5, + fillColor = colors.deepskyblue, strokeColor = None, strokeWidth=0) + g.add(bluebox1) + + whiteline1 = Rect(0, ((s)-stripeheight*3), width=stripeheight*5, height=stripeheight, + fillColor = colors.mintcream, strokeColor = None, strokeWidth=0) + g.add(whiteline1) + + whiteline2 = Rect((stripeheight*2), ((s)-stripeheight*5), width=stripeheight, height=stripeheight*5, + fillColor = colors.mintcream, strokeColor = None, strokeWidth=0) + g.add(whiteline2) + + return g + + def _Flag_Ireland(self): + s = _size + g = Group() + + box = Rect(0, 0, s*2, s, + fillColor = colors.forestgreen, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + whitebox = Rect(((s*2.0)/3.0), 0, width=(2.0*(s*2.0)/3.0), height=s, + fillColor = colors.mintcream, strokeColor = None, strokeWidth=0) + g.add(whitebox) + + orangebox = Rect(((2.0*(s*2.0)/3.0)), 0, width=(s*2.0)/3.0, height=s, + fillColor = colors.darkorange, strokeColor = None, strokeWidth=0) + g.add(orangebox) + return g + + def _Flag_Italy(self): + s = _size + g = Group() + g.add(Rect(0,0,s*2,s,fillColor=colors.forestgreen,strokeColor=None, strokeWidth=0)) + g.add(Rect((2*s)/3, 0, width=(s*4)/3, height=s, fillColor = colors.mintcream, strokeColor = None, strokeWidth=0)) + g.add(Rect((4*s)/3, 0, width=(s*2)/3, height=s, fillColor = colors.red, strokeColor = None, strokeWidth=0)) + return g + + def _Flag_Japan(self): + s = _size + g = Group() + w = self._width = s*1.5 + g.add(Rect(0,0,w,s,fillColor=colors.mintcream,strokeColor=None, strokeWidth=0)) + g.add(Circle(cx=w/2,cy=s/2,r=0.3*w,fillColor=colors.red,strokeColor=None, strokeWidth=0)) + return g + + def _Flag_Luxembourg(self): + s = _size + g = Group() + + box = Rect(0, 0, s*2, s, + fillColor = colors.mintcream, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + redbox = Rect(0, ((s/3.0)*2.0), width=s*2.0, height=s/3.0, + fillColor = colors.red, strokeColor = None, strokeWidth=0) + g.add(redbox) + + bluebox = Rect(0, 0, width=s*2.0, height=s/3.0, + fillColor = colors.dodgerblue, strokeColor = None, strokeWidth=0) + g.add(bluebox) + return g + + def _Flag_Holland(self): + s = _size + g = Group() + + box = Rect(0, 0, s*2, s, + fillColor = colors.mintcream, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + redbox = Rect(0, ((s/3.0)*2.0), width=s*2.0, height=s/3.0, + fillColor = colors.red, strokeColor = None, strokeWidth=0) + g.add(redbox) + + bluebox = Rect(0, 0, width=s*2.0, height=s/3.0, + fillColor = colors.darkblue, strokeColor = None, strokeWidth=0) + g.add(bluebox) + return g + + def _Flag_Portugal(self): + return Group() + + def _Flag_Russia(self): + s = _size + g = Group() + w = self._width = s*1.5 + t = s/3 + g.add(Rect(0, 0, width=w, height=t, fillColor = colors.red, strokeColor = None, strokeWidth=0)) + g.add(Rect(0, t, width=w, height=t, fillColor = colors.blue, strokeColor = None, strokeWidth=0)) + g.add(Rect(0, 2*t, width=w, height=t, fillColor = colors.mintcream, strokeColor = None, strokeWidth=0)) + return g + + def _Flag_Spain(self): + s = _size + g = Group() + w = self._width = s*1.5 + g.add(Rect(0, 0, width=w, height=s, fillColor = colors.red, strokeColor = None, strokeWidth=0)) + g.add(Rect(0, (s/4), width=w, height=s/2, fillColor = colors.yellow, strokeColor = None, strokeWidth=0)) + return g + + def _Flag_Sweden(self): + s = _size + g = Group() + self._width = s*1.4 + box = Rect(0, 0, self._width, s, + fillColor = colors.dodgerblue, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + box1 = Rect(((s/5)*2), 0, width=s/6, height=s, + fillColor = colors.gold, strokeColor = None, strokeWidth=0) + g.add(box1) + + box2 = Rect(0, ((s/2)-(s/12)), width=self._width, height=s/6, + fillColor = colors.gold, + strokeColor = None, + strokeWidth=0) + g.add(box2) + return g + + def _Flag_Norway(self): + s = _size + g = Group() + self._width = s*1.4 + + box = Rect(0, 0, self._width, s, + fillColor = colors.red, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + box = Rect(0, 0, self._width, s, + fillColor = colors.red, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + whiteline1 = Rect(((s*0.2)*2), 0, width=s*0.2, height=s, + fillColor = colors.ghostwhite, strokeColor = None, strokeWidth=0) + g.add(whiteline1) + + whiteline2 = Rect(0, (s*0.4), width=self._width, height=s*0.2, + fillColor = colors.ghostwhite, strokeColor = None, strokeWidth=0) + g.add(whiteline2) + + blueline1 = Rect(((s*0.225)*2), 0, width=0.1*s, height=s, + fillColor = colors.darkblue, strokeColor = None, strokeWidth=0) + g.add(blueline1) + + blueline2 = Rect(0, (s*0.45), width=self._width, height=s*0.1, + fillColor = colors.darkblue, strokeColor = None, strokeWidth=0) + g.add(blueline2) + return g + + def _Flag_CzechRepublic(self): + s = _size + g = Group() + box = Rect(0, 0, s*2, s, + fillColor = colors.mintcream, + strokeColor = colors.black, + strokeWidth=0) + g.add(box) + + redbox = Rect(0, 0, width=s*2, height=s/2, + fillColor = colors.red, + strokeColor = None, + strokeWidth=0) + g.add(redbox) + + bluewedge = Polygon(points = [ 0, 0, s, (s/2), 0, s], + fillColor = colors.darkblue, strokeColor = None, strokeWidth=0) + g.add(bluewedge) + return g + + def _Flag_Palestine(self): + s = _size + g = Group() + box = Rect(0, s/3, s*2, s/3, + fillColor = colors.mintcream, + strokeColor = None, + strokeWidth=0) + g.add(box) + + greenbox = Rect(0, 0, width=s*2, height=s/3, + fillColor = colors.limegreen, + strokeColor = None, + strokeWidth=0) + g.add(greenbox) + + blackbox = Rect(0, 2*s/3, width=s*2, height=s/3, + fillColor = colors.black, + strokeColor = None, + strokeWidth=0) + g.add(blackbox) + + redwedge = Polygon(points = [ 0, 0, 2*s/3, (s/2), 0, s], + fillColor = colors.red, strokeColor = None, strokeWidth=0) + g.add(redwedge) + return g + + def _Flag_Turkey(self): + s = _size + g = Group() + + box = Rect(0, 0, s*2, s, + fillColor = colors.red, + strokeColor = colors.black, + strokeWidth=0) + g.add(box) + + whitecircle = Circle(cx=((s*0.35)*2), cy=s/2, r=s*0.3, + fillColor = colors.mintcream, + strokeColor = None, + strokeWidth=0) + g.add(whitecircle) + + redcircle = Circle(cx=((s*0.39)*2), cy=s/2, r=s*0.24, + fillColor = colors.red, + strokeColor = None, + strokeWidth=0) + g.add(redcircle) + + ws = Star() + ws.angle = 15 + ws.size = s/5 + ws.x = (s*0.5)*2+ws.size/2 + ws.y = (s*0.5) + ws.fillColor = colors.mintcream + ws.strokeColor = None + g.add(ws) + return g + + def _Flag_Switzerland(self): + s = _size + g = Group() + self._width = s + + g.add(Rect(0, 0, s, s, fillColor = colors.red, strokeColor = colors.black, strokeWidth=0)) + g.add(Line((s/2), (s/5.5), (s/2), (s-(s/5.5)), + fillColor = colors.mintcream, strokeColor = colors.mintcream, strokeWidth=(s/5))) + g.add(Line((s/5.5), (s/2), (s-(s/5.5)), (s/2), + fillColor = colors.mintcream, strokeColor = colors.mintcream, strokeWidth=s/5)) + return g + + def _Flag_EU(self): + s = _size + g = Group() + w = self._width = 1.5*s + + g.add(Rect(0, 0, w, s, fillColor = colors.darkblue, strokeColor = None, strokeWidth=0)) + centerx=w/2 + centery=s/2 + radius=s/3 + yradius = radius + xradius = radius + nStars = 12 + delta = 2*pi/nStars + for i in range(nStars): + rad = i*delta + gs = Star() + gs.x=cos(rad)*radius+centerx + gs.y=sin(rad)*radius+centery + gs.size=s/10 + gs.fillColor=colors.gold + g.add(gs) + return g + + def _Flag_Brazil(self): + s = _size # abbreviate as we will use this a lot + g = Group() + + m = s/14 + self._width = w = (m * 20) + + def addStar(x,y,size, g=g, w=w, s=s, m=m): + st = Star() + st.fillColor=colors.mintcream + st.size = size*m + st.x = (w/2) + (x * (0.35 * m)) + st.y = (s/2) + (y * (0.35 * m)) + g.add(st) + + g.add(Rect(0, 0, w, s, fillColor = colors.green, strokeColor = None, strokeWidth=0)) + g.add(Polygon(points = [ 1.7*m, (s/2), (w/2), s-(1.7*m), w-(1.7*m),(s/2),(w/2), 1.7*m], + fillColor = colors.yellow, strokeColor = None, strokeWidth=0)) + g.add(Circle(cx=w/2, cy=s/2, r=3.5*m, + fillColor=colors.blue,strokeColor=None, strokeWidth=0)) + g.add(Wedge((w/2)-(2*m), 0, 8.5*m, 50, 98.1, 8.5*m, + fillColor=colors.mintcream,strokeColor=None, strokeWidth=0)) + g.add(Wedge((w/2), (s/2), 3.501*m, 156, 352, 3.501*m, + fillColor=colors.mintcream,strokeColor=None, strokeWidth=0)) + g.add(Wedge((w/2)-(2*m), 0, 8*m, 48.1, 100, 8*m, + fillColor=colors.blue,strokeColor=None, strokeWidth=0)) + g.add(Rect(0, 0, w, (s/4) + 1.7*m, + fillColor = colors.green, strokeColor = None, strokeWidth=0)) + g.add(Polygon(points = [ 1.7*m,(s/2), (w/2),s/2 - 2*m, w-(1.7*m),(s/2) , (w/2),1.7*m], + fillColor = colors.yellow, strokeColor = None, strokeWidth=0)) + g.add(Wedge(w/2, s/2, 3.502*m, 166, 342.1, 3.502*m, + fillColor=colors.blue,strokeColor=None, strokeWidth=0)) + + addStar(3.2,3.5,0.3) + addStar(-8.5,1.5,0.3) + addStar(-7.5,-3,0.3) + addStar(-4,-5.5,0.3) + addStar(0,-4.5,0.3) + addStar(7,-3.5,0.3) + addStar(-3.5,-0.5,0.25) + addStar(0,-1.5,0.25) + addStar(1,-2.5,0.25) + addStar(3,-7,0.25) + addStar(5,-6.5,0.25) + addStar(6.5,-5,0.25) + addStar(7,-4.5,0.25) + addStar(-5.5,-3.2,0.25) + addStar(-6,-4.2,0.25) + addStar(-1,-2.75,0.2) + addStar(2,-5.5,0.2) + addStar(4,-5.5,0.2) + addStar(5,-7.5,0.2) + addStar(5,-5.5,0.2) + addStar(6,-5.5,0.2) + addStar(-8.8,-3.2,0.2) + addStar(2.5,0.5,0.2) + addStar(-0.2,-3.2,0.14) + addStar(-7.2,-2,0.14) + addStar(0,-8,0.1) + + sTmp = "ORDEM E PROGRESSO" + nTmp = len(sTmp) + delta = 0.850848010347/nTmp + radius = 7.9 *m + centerx = (w/2)-(2*m) + centery = 0 + for i in range(nTmp): + rad = 2*pi - i*delta -4.60766922527 + x=cos(rad)*radius+centerx + y=sin(rad)*radius+centery + if i == 6: + z = 0.35*m + else: + z= 0.45*m + g2 = Group(String(x, y, sTmp[i], fontName='Helvetica-Bold', + fontSize = z,strokeColor=None,fillColor=colors.green)) + g2.rotate(rad) + g.add(g2) + return g + +def makeFlag(name): + flag = Flag() + flag.kind = name + return flag + +def test(): + """This function produces three pdf files with examples of all the signs and symbols from this file. + """ +# page 1 + + labelFontSize = 10 + + X = (20,245) + + flags = [ + 'UK', + 'USA', + 'Afghanistan', + 'Austria', + 'Belgium', + 'Denmark', + 'Cuba', + 'Finland', + 'France', + 'Germany', + 'Greece', + 'Ireland', + 'Italy', + 'Luxembourg', + 'Holland', + 'Palestine', + 'Portugal', + 'Spain', + 'Sweden', + 'Norway', + 'CzechRepublic', + 'Turkey', + 'Switzerland', + 'EU', + 'Brazil', + ] + y = Y0 = 530 + f = 0 + D = None + for name in flags: + if not D: D = Drawing(450,650) + flag = makeFlag(name) + i = flags.index(name) + flag.x = X[i%2] + flag.y = y + D.add(flag) + D.add(String(flag.x+(flag.size/2),(flag.y-(1.2*labelFontSize)), + name, fillColor=colors.black, textAnchor='middle', fontSize=labelFontSize)) + if i%2: y = y - 125 + if (i%2 and y<0) or name==flags[-1]: + renderPDF.drawToFile(D, 'flags%02d.pdf'%f, 'flags.py - Page #%d'%(f+1)) + y = Y0 + f = f+1 + D = None + +if __name__=='__main__': + test() diff --git a/bin/reportlab/graphics/widgets/grids.py b/bin/reportlab/graphics/widgets/grids.py new file mode 100644 index 00000000000..d3072728a89 --- /dev/null +++ b/bin/reportlab/graphics/widgets/grids.py @@ -0,0 +1,504 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/widgets/grids.py +__version__=''' $Id: grids.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' + +from reportlab.lib import colors +from reportlab.lib.validators import isNumber, isColorOrNone, isBoolean, isListOfNumbers, OneOf, isListOfColors +from reportlab.lib.attrmap import AttrMap, AttrMapValue +from reportlab.graphics.shapes import Drawing, Group, Line, Rect, LineShape, definePath, EmptyClipPath +from reportlab.graphics.widgetbase import Widget + +def frange(start, end=None, inc=None): + "A range function, that does accept float increments..." + + if end == None: + end = start + 0.0 + start = 0.0 + + if inc == None: + inc = 1.0 + + L = [] + end = end - inc*0.0001 #to avoid numrical problems + while 1: + next = start + len(L) * inc + if inc > 0 and next >= end: + break + elif inc < 0 and next <= end: + break + L.append(next) + + return L + + +def makeDistancesList(list): + """Returns a list of distances between adjacent numbers in some input list. + + E.g. [1, 1, 2, 3, 5, 7] -> [0, 1, 1, 2, 2] + """ + + d = [] + for i in range(len(list[:-1])): + d.append(list[i+1] - list[i]) + + return d + + +class Grid(Widget): + """This makes a rectangular grid of equidistant stripes. + + The grid contains an outer border rectangle, and stripes + inside which can be drawn with lines and/or as solid tiles. + The drawing order is: outer rectangle, then lines and tiles. + + The stripes' width is indicated as 'delta'. The sequence of + stripes can have an offset named 'delta0'. Both values need + to be positive! + """ + + _attrMap = AttrMap( + x = AttrMapValue(isNumber, desc="The grid's lower-left x position."), + y = AttrMapValue(isNumber, desc="The grid's lower-left y position."), + width = AttrMapValue(isNumber, desc="The grid's width."), + height = AttrMapValue(isNumber, desc="The grid's height."), + orientation = AttrMapValue(OneOf(('vertical', 'horizontal')), + desc='Determines if stripes are vertical or horizontal.'), + useLines = AttrMapValue(OneOf((0, 1)), + desc='Determines if stripes are drawn with lines.'), + useRects = AttrMapValue(OneOf((0, 1)), + desc='Determines if stripes are drawn with solid rectangles.'), + delta = AttrMapValue(isNumber, + desc='Determines the width/height of the stripes.'), + delta0 = AttrMapValue(isNumber, + desc='Determines the stripes initial width/height offset.'), + deltaSteps = AttrMapValue(isListOfNumbers, + desc='List of deltas to be used cyclically.'), + stripeColors = AttrMapValue(isListOfColors, + desc='Colors applied cyclically in the right or upper direction.'), + fillColor = AttrMapValue(isColorOrNone, + desc='Background color for entire rectangle.'), + strokeColor = AttrMapValue(isColorOrNone, + desc='Color used for lines.'), + strokeWidth = AttrMapValue(isNumber, + desc='Width used for lines.'), + rectStrokeColor = AttrMapValue(isColorOrNone, desc='Color for outer rect stroke.'), + rectStrokeWidth = AttrMapValue(isColorOrNone, desc='Width for outer rect stroke.'), + ) + + def __init__(self): + self.x = 0 + self.y = 0 + self.width = 100 + self.height = 100 + self.orientation = 'vertical' + self.useLines = 0 + self.useRects = 1 + self.delta = 20 + self.delta0 = 0 + self.deltaSteps = [] + self.fillColor = colors.white + self.stripeColors = [colors.red, colors.green, colors.blue] + self.strokeColor = colors.black + self.strokeWidth = 2 + + + def demo(self): + D = Drawing(100, 100) + + g = Grid() + D.add(g) + + return D + + def makeOuterRect(self): + strokeColor = getattr(self,'rectStrokeColor',self.strokeColor) + strokeWidth = getattr(self,'rectStrokeWidth',self.strokeWidth) + if self.fillColor or (strokeColor and strokeWidth): + rect = Rect(self.x, self.y, self.width, self.height) + rect.fillColor = self.fillColor + rect.strokeColor = strokeColor + rect.strokeWidth = strokeWidth + return rect + else: + return None + + def makeLinePosList(self, start, isX=0): + "Returns a list of positions where to place lines." + + w, h = self.width, self.height + if isX: + length = w + else: + length = h + if self.deltaSteps: + r = [start + self.delta0] + i = 0 + while 1: + if r[-1] > start + length: + del r[-1] + break + r.append(r[-1] + self.deltaSteps[i % len(self.deltaSteps)]) + i = i + 1 + else: + r = frange(start + self.delta0, start + length, self.delta) + + r.append(start + length) + if self.delta0 != 0: + r.insert(0, start) + #print 'Grid.makeLinePosList() -> %s' % r + return r + + + def makeInnerLines(self): + # inner grid lines + group = Group() + + w, h = self.width, self.height + + if self.useLines == 1: + if self.orientation == 'vertical': + r = self.makeLinePosList(self.x, isX=1) + for x in r: + line = Line(x, self.y, x, self.y + h) + line.strokeColor = self.strokeColor + line.strokeWidth = self.strokeWidth + group.add(line) + elif self.orientation == 'horizontal': + r = self.makeLinePosList(self.y, isX=0) + for y in r: + line = Line(self.x, y, self.x + w, y) + line.strokeColor = self.strokeColor + line.strokeWidth = self.strokeWidth + group.add(line) + + return group + + + def makeInnerTiles(self): + # inner grid lines + group = Group() + + w, h = self.width, self.height + + # inner grid stripes (solid rectangles) + if self.useRects == 1: + cols = self.stripeColors + + if self.orientation == 'vertical': + r = self.makeLinePosList(self.x, isX=1) + elif self.orientation == 'horizontal': + r = self.makeLinePosList(self.y, isX=0) + + dist = makeDistancesList(r) + + i = 0 + for j in range(len(dist)): + if self.orientation == 'vertical': + x = r[j] + stripe = Rect(x, self.y, dist[j], h) + elif self.orientation == 'horizontal': + y = r[j] + stripe = Rect(self.x, y, w, dist[j]) + stripe.fillColor = cols[i % len(cols)] + stripe.strokeColor = None + group.add(stripe) + i = i + 1 + + return group + + + def draw(self): + # general widget bits + group = Group() + + group.add(self.makeOuterRect()) + group.add(self.makeInnerTiles()) + group.add(self.makeInnerLines(),name='_gridLines') + + return group + + +class DoubleGrid(Widget): + """This combines two ordinary Grid objects orthogonal to each other. + """ + + _attrMap = AttrMap( + x = AttrMapValue(isNumber, desc="The grid's lower-left x position."), + y = AttrMapValue(isNumber, desc="The grid's lower-left y position."), + width = AttrMapValue(isNumber, desc="The grid's width."), + height = AttrMapValue(isNumber, desc="The grid's height."), + grid0 = AttrMapValue(None, desc="The first grid component."), + grid1 = AttrMapValue(None, desc="The second grid component."), + ) + + def __init__(self): + self.x = 0 + self.y = 0 + self.width = 100 + self.height = 100 + + g0 = Grid() + g0.x = self.x + g0.y = self.y + g0.width = self.width + g0.height = self.height + g0.orientation = 'vertical' + g0.useLines = 1 + g0.useRects = 0 + g0.delta = 20 + g0.delta0 = 0 + g0.deltaSteps = [] + g0.fillColor = colors.white + g0.stripeColors = [colors.red, colors.green, colors.blue] + g0.strokeColor = colors.black + g0.strokeWidth = 1 + + g1 = Grid() + g1.x = self.x + g1.y = self.y + g1.width = self.width + g1.height = self.height + g1.orientation = 'horizontal' + g1.useLines = 1 + g1.useRects = 0 + g1.delta = 20 + g1.delta0 = 0 + g1.deltaSteps = [] + g1.fillColor = colors.white + g1.stripeColors = [colors.red, colors.green, colors.blue] + g1.strokeColor = colors.black + g1.strokeWidth = 1 + + self.grid0 = g0 + self.grid1 = g1 + + +## # This gives an AttributeError: +## # DoubleGrid instance has no attribute 'grid0' +## def __setattr__(self, name, value): +## if name in ('x', 'y', 'width', 'height'): +## setattr(self.grid0, name, value) +## setattr(self.grid1, name, value) + + + def demo(self): + D = Drawing(100, 100) + g = DoubleGrid() + D.add(g) + return D + + + def draw(self): + group = Group() + g0, g1 = self.grid0, self.grid1 + # Order groups to make sure both v and h lines + # are visible (works only when there is only + # one kind of stripes, v or h). + G = g0.useRects == 1 and g1.useRects == 0 and (g0,g1) or (g1,g0) + for g in G: + group.add(g.makeOuterRect()) + for g in G: + group.add(g.makeInnerTiles()) + group.add(g.makeInnerLines(),name='_gridLines') + + return group + + +class ShadedRect(Widget): + """This makes a rectangle with shaded colors between two colors. + + Colors are interpolated linearly between 'fillColorStart' + and 'fillColorEnd', both of which appear at the margins. + If 'numShades' is set to one, though, only 'fillColorStart' + is used. + """ + + _attrMap = AttrMap( + x = AttrMapValue(isNumber, desc="The grid's lower-left x position."), + y = AttrMapValue(isNumber, desc="The grid's lower-left y position."), + width = AttrMapValue(isNumber, desc="The grid's width."), + height = AttrMapValue(isNumber, desc="The grid's height."), + orientation = AttrMapValue(OneOf(('vertical', 'horizontal')), desc='Determines if stripes are vertical or horizontal.'), + numShades = AttrMapValue(isNumber, desc='The number of interpolating colors.'), + fillColorStart = AttrMapValue(isColorOrNone, desc='Start value of the color shade.'), + fillColorEnd = AttrMapValue(isColorOrNone, desc='End value of the color shade.'), + strokeColor = AttrMapValue(isColorOrNone, desc='Color used for border line.'), + strokeWidth = AttrMapValue(isNumber, desc='Width used for lines.'), + cylinderMode = AttrMapValue(isBoolean, desc='True if shading reverses in middle.'), + ) + + def __init__(self,**kw): + self.x = 0 + self.y = 0 + self.width = 100 + self.height = 100 + self.orientation = 'vertical' + self.numShades = 20 + self.fillColorStart = colors.pink + self.fillColorEnd = colors.black + self.strokeColor = colors.black + self.strokeWidth = 2 + self.cylinderMode = 0 + self.setProperties(kw) + + def demo(self): + D = Drawing(100, 100) + g = ShadedRect() + D.add(g) + + return D + + def _flipRectCorners(self): + "Flip rectangle's corners if width or height is negative." + x, y, width, height, fillColorStart, fillColorEnd = self.x, self.y, self.width, self.height, self.fillColorStart, self.fillColorEnd + if width < 0 and height > 0: + x = x + width + width = -width + if self.orientation=='vertical': fillColorStart, fillColorEnd = fillColorEnd, fillColorStart + elif height<0 and width>0: + y = y + height + height = -height + if self.orientation=='horizontal': fillColorStart, fillColorEnd = fillColorEnd, fillColorStart + elif height < 0 and height < 0: + x = x + width + width = -width + y = y + height + height = -height + return x, y, width, height, fillColorStart, fillColorEnd + + def draw(self): + # general widget bits + group = Group() + x, y, w, h, c0, c1 = self._flipRectCorners() + numShades = self.numShades + if self.cylinderMode: + if not numShades%2: numShades = numShades+1 + halfNumShades = (numShades-1)/2 + 1 + num = float(numShades) # must make it float! + vertical = self.orientation == 'vertical' + if vertical: + if numShades == 1: + V = [x] + else: + V = frange(x, x + w, w/num) + else: + if numShades == 1: + V = [y] + else: + V = frange(y, y + h, h/num) + + for v in V: + stripe = vertical and Rect(v, y, w/num, h) or Rect(x, v, w, h/num) + if self.cylinderMode: + if V.index(v)>=halfNumShades: + col = colors.linearlyInterpolatedColor(c1,c0,V[halfNumShades],V[-1], v) + else: + col = colors.linearlyInterpolatedColor(c0,c1,V[0],V[halfNumShades], v) + else: + col = colors.linearlyInterpolatedColor(c0,c1,V[0],V[-1], v) + stripe.fillColor = col + stripe.strokeColor = col + stripe.strokeWidth = 1 + group.add(stripe) + if self.strokeColor and self.strokeWidth>=0: + rect = Rect(x, y, w, h) + rect.strokeColor = self.strokeColor + rect.strokeWidth = self.strokeWidth + rect.fillColor = None + group.add(rect) + return group + + +def colorRange(c0, c1, n): + "Return a range of intermediate colors between c0 and c1" + if n==1: return [c0] + + C = [] + if n>1: + lim = n-1 + for i in range(n): + C.append(colors.linearlyInterpolatedColor(c0,c1,0,lim, i)) + return C + + +def centroid(P): + '''compute average point of a set of points''' + return reduce(lambda x,y, fn=float(len(P)): (x[0]+y[0]/fn,x[1]+y[1]/fn),P,(0,0)) + +def rotatedEnclosingRect(P, angle, rect): + ''' + given P a sequence P of x,y coordinate pairs and an angle in degrees + find the centroid of P and the axis at angle theta through it + find the extreme points of P wrt axis parallel distance and axis + orthogonal distance. Then compute the least rectangle that will still + enclose P when rotated by angle. + + The class R + ''' + from math import pi, cos, sin, tan + x0, y0 = centroid(P) + theta = (angle/180.)*pi + s,c=sin(theta),cos(theta) + def parallelAxisDist((x,y),s=s,c=c,x0=x0,y0=y0): + return (s*(y-y0)+c*(x-x0)) + def orthogonalAxisDist((x,y),s=s,c=c,x0=x0,y0=y0): + return (c*(y-y0)+s*(x-x0)) + L = map(parallelAxisDist,P) + L.sort() + a0, a1 = L[0], L[-1] + L = map(orthogonalAxisDist,P) + L.sort() + b0, b1 = L[0], L[-1] + rect.x, rect.width = a0, a1-a0 + rect.y, rect.height = b0, b1-b0 + g = Group(transform=(c,s,-s,c,x0,y0)) + g.add(rect) + return g + +class ShadedPolygon(Widget,LineShape): + _attrMap = AttrMap(BASE=LineShape, + angle = AttrMapValue(isNumber,desc="Shading angle"), + fillColorStart = AttrMapValue(isColorOrNone), + fillColorEnd = AttrMapValue(isColorOrNone), + numShades = AttrMapValue(isNumber, desc='The number of interpolating colors.'), + cylinderMode = AttrMapValue(isBoolean, desc='True if shading reverses in middle.'), + points = AttrMapValue(isListOfNumbers), + ) + + def __init__(self,**kw): + self.angle = 90 + self.fillColorStart = colors.red + self.fillColorEnd = colors.green + self.cylinderMode = 0 + self.numShades = 50 + self.points = [-1,-1,2,2,3,-1] + LineShape.__init__(self,kw) + + def draw(self): + P = self.points + P = map(lambda i, P=P:(P[i],P[i+1]),xrange(0,len(P),2)) + path = definePath([('moveTo',)+P[0]]+map(lambda x: ('lineTo',)+x,P[1:])+['closePath'], + fillColor=None, strokeColor=None) + path.isClipPath = 1 + g = Group() + g.add(path) + rect = ShadedRect(strokeWidth=0,strokeColor=None) + for k in 'fillColorStart', 'fillColorEnd', 'numShades', 'cylinderMode': + setattr(rect,k,getattr(self,k)) + g.add(rotatedEnclosingRect(P, self.angle, rect)) + g.add(EmptyClipPath) + path = path.copy() + path.isClipPath = 0 + path.strokeColor = self.strokeColor + path.strokeWidth = self.strokeWidth + g.add(path) + return g + +if __name__=='__main__': #noruntests + from reportlab.lib.colors import blue + from reportlab.graphics.shapes import Drawing + angle=45 + D = Drawing(120,120) + D.add(ShadedPolygon(points=(10,10,60,60,110,10),strokeColor=None,strokeWidth=1,angle=90,numShades=50,cylinderMode=0)) + D.save(formats=['gif'],fnRoot='shobj',outDir='/tmp') diff --git a/bin/reportlab/graphics/widgets/markers.py b/bin/reportlab/graphics/widgets/markers.py new file mode 100644 index 00000000000..ed700773ab2 --- /dev/null +++ b/bin/reportlab/graphics/widgets/markers.py @@ -0,0 +1,228 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/widgets/markers.py +""" +This modules defines a collection of markers used in charts. +""" +__version__=''' $Id: markers.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' +from types import FunctionType, ClassType +from reportlab.graphics.shapes import Rect, Line, Circle, Polygon, Drawing, Group +from reportlab.graphics.widgets.signsandsymbols import SmileyFace +from reportlab.graphics.widgetbase import Widget +from reportlab.lib.validators import isNumber, isColorOrNone, OneOf, Validator +from reportlab.lib.attrmap import AttrMap, AttrMapValue +from reportlab.lib.colors import black +from reportlab.graphics.widgets.flags import Flag +from math import sin, cos, pi +import copy, new +_toradians = pi/180.0 + +class Marker(Widget): + '''A polymorphic class of markers''' + _attrMap = AttrMap(BASE=Widget, + kind = AttrMapValue( + OneOf(None, 'Square', 'Diamond', 'Circle', 'Cross', 'Triangle', 'StarSix', + 'Pentagon', 'Hexagon', 'Heptagon', 'Octagon', 'StarFive', + 'FilledSquare', 'FilledCircle', 'FilledDiamond', 'FilledCross', + 'FilledTriangle','FilledStarSix', 'FilledPentagon', 'FilledHexagon', + 'FilledHeptagon', 'FilledOctagon', 'FilledStarFive', + 'Smiley'), + desc='marker type name'), + size = AttrMapValue(isNumber,desc='marker size'), + x = AttrMapValue(isNumber,desc='marker x coordinate'), + y = AttrMapValue(isNumber,desc='marker y coordinate'), + dx = AttrMapValue(isNumber,desc='marker x coordinate adjustment'), + dy = AttrMapValue(isNumber,desc='marker y coordinate adjustment'), + angle = AttrMapValue(isNumber,desc='marker rotation'), + fillColor = AttrMapValue(isColorOrNone, desc='marker fill colour'), + strokeColor = AttrMapValue(isColorOrNone, desc='marker stroke colour'), + strokeWidth = AttrMapValue(isNumber, desc='marker stroke width'), + ) + + def __init__(self,*args,**kw): + self.kind = None + self.strokeColor = black + self.strokeWidth = 0.1 + self.fillColor = None + self.size = 5 + self.x = self.y = self.dx = self.dy = self.angle = 0 + self.setProperties(kw) + + def clone(self): + return new.instance(self.__class__,self.__dict__.copy()) + + def _Smiley(self): + x, y = self.x+self.dx, self.y+self.dy + d = self.size/2.0 + s = SmileyFace() + s.fillColor = self.fillColor + s.strokeWidth = self.strokeWidth + s.strokeColor = self.strokeColor + s.x = x-d + s.y = y-d + s.size = d*2 + return s + + def _Square(self): + x, y = self.x+self.dx, self.y+self.dy + d = self.size/2.0 + s = Rect(x-d,y-d,2*d,2*d,fillColor=self.fillColor,strokeColor=self.strokeColor,strokeWidth=self.strokeWidth) + return s + + def _Diamond(self): + d = self.size/2.0 + return self._doPolygon((-d,0,0,d,d,0,0,-d)) + + def _Circle(self): + x, y = self.x+self.dx, self.y+self.dy + s = Circle(x,y,self.size/2.0,fillColor=self.fillColor,strokeColor=self.strokeColor,strokeWidth=self.strokeWidth) + return s + + def _Cross(self): + x, y = self.x+self.dx, self.y+self.dy + s = float(self.size) + h, s = s/2, s/6 + return self._doPolygon((-s,-h,-s,-s,-h,-s,-h,s,-s,s,-s,h,s,h,s,s,h,s,h,-s,s,-s,s,-h)) + + def _Triangle(self): + x, y = self.x+self.dx, self.y+self.dy + r = float(self.size)/2 + c = 30*_toradians + s = sin(30*_toradians)*r + c = cos(c)*r + return self._doPolygon((0,r,-c,-s,c,-s)) + + def _StarSix(self): + r = float(self.size)/2 + c = 30*_toradians + s = sin(c)*r + c = cos(c)*r + z = s/2 + g = c/2 + return self._doPolygon((0,r,-z,s,-c,s,-s,0,-c,-s,-z,-s,0,-r,z,-s,c,-s,s,0,c,s,z,s)) + + def _StarFive(self): + R = float(self.size)/2 + r = R*sin(18*_toradians)/cos(36*_toradians) + P = [] + angle = 90 + for i in xrange(5): + for radius in R, r: + theta = angle*_toradians + P.append(radius*cos(theta)) + P.append(radius*sin(theta)) + angle = angle + 36 + return self._doPolygon(P) + + def _Pentagon(self): + return self._doNgon(5) + + def _Hexagon(self): + return self._doNgon(6) + + def _Heptagon(self): + return self._doNgon(7) + + def _Octagon(self): + return self._doNgon(8) + + def _doPolygon(self,P): + x, y = self.x+self.dx, self.y+self.dy + if x or y: P = map(lambda i,P=P,A=[x,y]: P[i] + A[i&1], range(len(P))) + return Polygon(P, strokeWidth =self.strokeWidth, strokeColor=self.strokeColor, fillColor=self.fillColor) + + def _doFill(self): + old = self.fillColor + if old is None: + self.fillColor = self.strokeColor + r = (self.kind and getattr(self,'_'+self.kind[6:]) or Group)() + self.fillColor = old + return r + + def _doNgon(self,n): + P = [] + size = float(self.size)/2 + for i in xrange(n): + r = (2.*i/n+0.5)*pi + P.append(size*cos(r)) + P.append(size*sin(r)) + return self._doPolygon(P) + + _FilledCircle = _doFill + _FilledSquare = _doFill + _FilledDiamond = _doFill + _FilledCross = _doFill + _FilledTriangle = _doFill + _FilledStarSix = _doFill + _FilledPentagon = _doFill + _FilledHexagon = _doFill + _FilledHeptagon = _doFill + _FilledOctagon = _doFill + _FilledStarFive = _doFill + + def draw(self): + if self.kind: + m = getattr(self,'_'+self.kind) + if self.angle: + _x, _dx, _y, _dy = self.x, self.dx, self.y, self.dy + self.x, self.dx, self.y, self.dy = 0,0,0,0 + try: + m = m() + finally: + self.x, self.dx, self.y, self.dy = _x, _dx, _y, _dy + if not isinstance(m,Group): + _m, m = m, Group() + m.add(_m) + if self.angle: m.rotate(self.angle) + x, y = _x+_dx, _y+_dy + if x or y: m.shift(x,y) + else: + m = m() + else: + m = Group() + return m + +def uSymbol2Symbol(uSymbol,x,y,color): + if type(uSymbol) == FunctionType: + symbol = uSymbol(x, y, 5, color) + elif type(uSymbol) == ClassType and issubclass(uSymbol,Widget): + size = 10. + symbol = uSymbol() + symbol.x = x - (size/2) + symbol.y = y - (size/2) + try: + symbol.size = size + symbol.color = color + except: + pass + elif isinstance(uSymbol,Marker) or isinstance(uSymbol,Flag): + symbol = uSymbol.clone() + if isinstance(uSymbol,Marker): symbol.fillColor = symbol.fillColor or color + symbol.x, symbol.y = x, y + else: + symbol = None + return symbol + +class _isSymbol(Validator): + def test(self,x): + return callable(x) or isinstance(x,Marker) or isinstance(x,Flag) \ + or (type(x)==ClassType and issubclass(x,Widget)) + +isSymbol = _isSymbol() + +def makeMarker(name,**kw): + if Marker._attrMap['kind'].validate(name): + m = apply(Marker,(),kw) + m.kind = name + elif name[-5:]=='_Flag' and Flag._attrMap['kind'].validate(name[:-5]): + m = apply(Flag,(),kw) + m.kind = name[:-5] + m.size = 10 + else: + raise ValueError, "Invalid marker name %s" % name + return m + +if __name__=='__main__': + D = Drawing() + D.add(Marker()) + D.save(fnRoot='Marker',formats=['pdf'], outDir='/tmp') diff --git a/bin/reportlab/graphics/widgets/signsandsymbols.py b/bin/reportlab/graphics/widgets/signsandsymbols.py new file mode 100644 index 00000000000..f7f193cf536 --- /dev/null +++ b/bin/reportlab/graphics/widgets/signsandsymbols.py @@ -0,0 +1,919 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/widgets/signsandsymbols.py +# signsandsymbols.py +# A collection of new widgets +# author: John Precedo (johnp@reportlab.com) +"""This file is a collection of widgets to produce some common signs and symbols. + +Widgets include: +- ETriangle (an equilateral triangle), +- RTriangle (a right angled triangle), +- Octagon, +- Crossbox, +- Tickbox, +- SmileyFace, +- StopSign, +- NoEntry, +- NotAllowed (the red roundel from 'no smoking' signs), +- NoSmoking, +- DangerSign (a black exclamation point in a yellow triangle), +- YesNo (returns a tickbox or a crossbox depending on a testvalue), +- FloppyDisk, +- ArrowOne, and +- ArrowTwo +""" +__version__=''' $Id: signsandsymbols.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' + +from reportlab.lib import colors +from reportlab.lib.validators import * +from reportlab.lib.attrmap import * +from reportlab.graphics import shapes +from reportlab.graphics.widgetbase import Widget +from reportlab.graphics import renderPDF + + +class _Symbol(Widget): + """Abstract base widget + possible attributes: + 'x', 'y', 'size', 'fillColor', 'strokeColor' + """ + _nodoc = 1 + _attrMap = AttrMap( + x = AttrMapValue(isNumber,desc='symbol x coordinate'), + y = AttrMapValue(isNumber,desc='symbol y coordinate'), + dx = AttrMapValue(isNumber,desc='symbol x coordinate adjustment'), + dy = AttrMapValue(isNumber,desc='symbol x coordinate adjustment'), + size = AttrMapValue(isNumber), + fillColor = AttrMapValue(isColorOrNone), + strokeColor = AttrMapValue(isColorOrNone), + strokeWidth = AttrMapValue(isNumber), + ) + def __init__(self): + assert self.__class__.__name__!='_Symbol', 'Abstract class _Symbol instantiated' + self.x = self.y = self.dx = self.dy = 0 + self.size = 100 + self.fillColor = colors.red + self.strokeColor = None + self.strokeWidth = 0.1 + + def demo(self): + D = shapes.Drawing(200, 100) + s = float(self.size) + ob = self.__class__() + ob.x=50 + ob.y=0 + ob.draw() + D.add(ob) + D.add(shapes.String(ob.x+(s/2),(ob.y-12), + ob.__class__.__name__, fillColor=colors.black, textAnchor='middle', + fontSize=10)) + return D + +class ETriangle(_Symbol): + """This draws an equilateral triangle.""" + + def __init__(self): + pass #AbstractSymbol + + def draw(self): + # general widget bits + s = float(self.size) # abbreviate as we will use this a lot + g = shapes.Group() + + # Triangle specific bits + ae = s*0.125 #(ae = 'an eighth') + triangle = shapes.Polygon(points = [ + self.x, self.y, + self.x+s, self.y, + self.x+(s/2),self.y+s], + fillColor = self.fillColor, + strokeColor = self.strokeColor, + strokeWidth=s/50.) + g.add(triangle) + return g + +class RTriangle(_Symbol): + """This draws a right-angled triangle. + + possible attributes: + 'x', 'y', 'size', 'fillColor', 'strokeColor' + + """ + + def __init__(self): + self.x = 0 + self.y = 0 + self.size = 100 + self.fillColor = colors.green + self.strokeColor = None + + def draw(self): + # general widget bits + s = float(self.size) # abbreviate as we will use this a lot + g = shapes.Group() + + # Triangle specific bits + ae = s*0.125 #(ae = 'an eighth') + triangle = shapes.Polygon(points = [ + self.x, self.y, + self.x+s, self.y, + self.x,self.y+s], + fillColor = self.fillColor, + strokeColor = self.strokeColor, + strokeWidth=s/50.) + g.add(triangle) + return g + +class Octagon(_Symbol): + """This widget draws an Octagon. + + possible attributes: + 'x', 'y', 'size', 'fillColor', 'strokeColor' + + """ + + def __init__(self): + self.x = 0 + self.y = 0 + self.size = 100 + self.fillColor = colors.yellow + self.strokeColor = None + + def draw(self): + # general widget bits + s = float(self.size) # abbreviate as we will use this a lot + g = shapes.Group() + + # Octagon specific bits + athird=s/3 + + octagon = shapes.Polygon(points=[self.x+athird, self.y, + self.x, self.y+athird, + self.x, self.y+(athird*2), + self.x+athird, self.y+s, + self.x+(athird*2), self.y+s, + self.x+s, self.y+(athird*2), + self.x+s, self.y+athird, + self.x+(athird*2), self.y], + strokeColor = self.strokeColor, + fillColor = self.fillColor, + strokeWidth=10) + g.add(octagon) + return g + +class Crossbox(_Symbol): + """This draws a black box with a red cross in it - a 'checkbox'. + + possible attributes: + 'x', 'y', 'size', 'crossColor', 'strokeColor', 'crosswidth' + + """ + + _attrMap = AttrMap(BASE=_Symbol, + crossColor = AttrMapValue(isColorOrNone), + crosswidth = AttrMapValue(isNumber), + ) + + def __init__(self): + self.x = 0 + self.y = 0 + self.size = 100 + self.fillColor = colors.white + self.crossColor = colors.red + self.strokeColor = colors.black + self.crosswidth = 10 + + def draw(self): + # general widget bits + s = float(self.size) # abbreviate as we will use this a lot + g = shapes.Group() + + # crossbox specific bits + box = shapes.Rect(self.x+1, self.y+1, s-2, s-2, + fillColor = self.fillColor, + strokeColor = self.strokeColor, + strokeWidth=2) + g.add(box) + + crossLine1 = shapes.Line(self.x+(s*0.15), self.y+(s*0.15), self.x+(s*0.85), self.y+(s*0.85), + fillColor = self.crossColor, + strokeColor = self.crossColor, + strokeWidth = self.crosswidth) + g.add(crossLine1) + + crossLine2 = shapes.Line(self.x+(s*0.15), self.y+(s*0.85), self.x+(s*0.85) ,self.y+(s*0.15), + fillColor = self.crossColor, + strokeColor = self.crossColor, + strokeWidth = self.crosswidth) + g.add(crossLine2) + + return g + + +class Tickbox(_Symbol): + """This draws a black box with a red tick in it - another 'checkbox'. + + possible attributes: + 'x', 'y', 'size', 'tickColor', 'strokeColor', 'tickwidth' + +""" + + _attrMap = AttrMap(BASE=_Symbol, + tickColor = AttrMapValue(isColorOrNone), + tickwidth = AttrMapValue(isNumber), + ) + + def __init__(self): + self.x = 0 + self.y = 0 + self.size = 100 + self.tickColor = colors.red + self.strokeColor = colors.black + self.fillColor = colors.white + self.tickwidth = 10 + + def draw(self): + # general widget bits + s = float(self.size) # abbreviate as we will use this a lot + g = shapes.Group() + + # tickbox specific bits + box = shapes.Rect(self.x+1, self.y+1, s-2, s-2, + fillColor = self.fillColor, + strokeColor = self.strokeColor, + strokeWidth=2) + g.add(box) + + tickLine = shapes.PolyLine(points = [self.x+(s*0.15), self.y+(s*0.35), self.x+(s*0.35), self.y+(s*0.15), + self.x+(s*0.35), self.y+(s*0.15), self.x+(s*0.85) ,self.y+(s*0.85)], + fillColor = self.tickColor, + strokeColor = self.tickColor, + strokeWidth = self.tickwidth) + g.add(tickLine) + + return g + +class SmileyFace(_Symbol): + """This draws a classic smiley face. + + possible attributes: + 'x', 'y', 'size', 'fillColor' + + """ + + def __init__(self): + _Symbol.__init__(self) + self.x = 0 + self.y = 0 + self.size = 100 + self.fillColor = colors.yellow + self.strokeColor = colors.black + + def draw(self): + # general widget bits + s = float(self.size) # abbreviate as we will use this a lot + g = shapes.Group() + + # SmileyFace specific bits + g.add(shapes.Circle(cx=self.x+(s/2), cy=self.y+(s/2), r=s/2, + fillColor=self.fillColor, strokeColor=self.strokeColor, + strokeWidth=max(s/38.,self.strokeWidth))) + + for i in (1,2): + g.add(shapes.Ellipse(self.x+(s/3)*i,self.y+(s/3)*2, s/30, s/10, + fillColor=self.strokeColor, strokeColor = self.strokeColor, + strokeWidth=max(s/38.,self.strokeWidth))) + + # calculate a pointslist for the mouth + # THIS IS A HACK! - don't use if there is a 'shapes.Arc' + centerx=self.x+(s/2) + centery=self.y+(s/2) + radius=s/3 + yradius = radius + xradius = radius + startangledegrees=200 + endangledegrees=340 + degreedelta = 1 + pointslist = [] + a = pointslist.append + from math import sin, cos, pi + degreestoradians = pi/180.0 + radiansdelta = degreedelta*degreestoradians + startangle = startangledegrees*degreestoradians + endangle = endangledegrees*degreestoradians + while endangle