[reportlab-users] Wrapped Labels Pie Chart

Marc Stober reportlab-users@reportlab.com
Wed, 23 Apr 2003 13:24:07 -0400


This message is in MIME format. Since your mail reader does not understand
this format, some or all of this message may not be legible.

------_=_NextPart_000_01C309BD.251FB610
Content-Type: multipart/alternative;
	boundary="----_=_NextPart_001_01C309BD.251FB610"


------_=_NextPart_001_01C309BD.251FB610
Content-Type: text/plain


Daniel and Troy, 

Here is a pie chart with wrapped labels (using some internals of the
Paragraph class as a wrapping algorithm). Hope you find it useful. Feel free
to contact me with any questions you have or bugs you find.

Thanks,
Marc Stober
mstober@dalbar.com
 

> -----Original Message-----
> From: Daniel McQuillen [mailto:daniel@bluepattern.com]
> Sent: Tuesday, April 22, 2003 3:27 PM
> To: Troy Sorzano; Marc Stober
> Subject: Re: [reportlab-users] RE: reportlab-users digest, 
> Vol 1 #293 -
> 2 msgs
> 
> 
> Troy Sorzano wrote:
> 
> >>>1. I have made a *pie* chart class, based on the standard 
> ReportLab pie chart class but with some added wrapping of the 
> labels. If you are interested I will send you a copy.<<
> >>>      
> >>>
> >
> >
> >Hi Daniel,
> >
> >Could you send me your pie chart class.  I am looking for 
> the ability to wrap labels.
> >
> >Thanks,
> >
> >Troy Sorzano
> >Info Packaging
> >


------_=_NextPart_001_01C309BD.251FB610
Content-Type: text/html
Content-Transfer-Encoding: quoted-printable

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN">
<HTML>
<HEAD>
<META HTTP-EQUIV=3D"Content-Type" CONTENT=3D"text/html; =
charset=3DUS-ASCII">
<META NAME=3D"Generator" CONTENT=3D"MS Exchange Server version =
5.5.2653.12">
<TITLE>Wrapped Labels Pie Chart</TITLE>
</HEAD>
<BODY>
<BR>

<P><FONT SIZE=3D2>Daniel and Troy, </FONT>
</P>

<P><FONT SIZE=3D2>Here is a pie chart with wrapped labels (using some =
internals of the Paragraph class as a wrapping algorithm). Hope you =
find it useful. Feel free to contact me with any questions you have or =
bugs you find.</FONT></P>

<P><FONT SIZE=3D2>Thanks,</FONT>
<BR><FONT SIZE=3D2>Marc Stober</FONT>
<BR><FONT SIZE=3D2>mstober@dalbar.com</FONT>
<BR><FONT SIZE=3D2>&nbsp;</FONT>
</P>

<P><FONT SIZE=3D2>&gt; -----Original Message-----</FONT>
<BR><FONT SIZE=3D2>&gt; From: Daniel McQuillen [<A =
HREF=3D"mailto:daniel@bluepattern.com">mailto:daniel@bluepattern.com</A>=
]</FONT>
<BR><FONT SIZE=3D2>&gt; Sent: Tuesday, April 22, 2003 3:27 PM</FONT>
<BR><FONT SIZE=3D2>&gt; To: Troy Sorzano; Marc Stober</FONT>
<BR><FONT SIZE=3D2>&gt; Subject: Re: [reportlab-users] RE: =
reportlab-users digest, </FONT>
<BR><FONT SIZE=3D2>&gt; Vol 1 #293 -</FONT>
<BR><FONT SIZE=3D2>&gt; 2 msgs</FONT>
<BR><FONT SIZE=3D2>&gt; </FONT>
<BR><FONT SIZE=3D2>&gt; </FONT>
<BR><FONT SIZE=3D2>&gt; Troy Sorzano wrote:</FONT>
<BR><FONT SIZE=3D2>&gt; </FONT>
<BR><FONT SIZE=3D2>&gt; &gt;&gt;&gt;1. I have made a *pie* chart class, =
based on the standard </FONT>
<BR><FONT SIZE=3D2>&gt; ReportLab pie chart class but with some added =
wrapping of the </FONT>
<BR><FONT SIZE=3D2>&gt; labels. If you are interested I will send you a =
copy.&lt;&lt;</FONT>
<BR><FONT SIZE=3D2>&gt; &gt;&gt;&gt;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; =
</FONT>
<BR><FONT SIZE=3D2>&gt; &gt;&gt;&gt;</FONT>
<BR><FONT SIZE=3D2>&gt; &gt;</FONT>
<BR><FONT SIZE=3D2>&gt; &gt;</FONT>
<BR><FONT SIZE=3D2>&gt; &gt;Hi Daniel,</FONT>
<BR><FONT SIZE=3D2>&gt; &gt;</FONT>
<BR><FONT SIZE=3D2>&gt; &gt;Could you send me your pie chart =
class.&nbsp; I am looking for </FONT>
<BR><FONT SIZE=3D2>&gt; the ability to wrap labels.</FONT>
<BR><FONT SIZE=3D2>&gt; &gt;</FONT>
<BR><FONT SIZE=3D2>&gt; &gt;Thanks,</FONT>
<BR><FONT SIZE=3D2>&gt; &gt;</FONT>
<BR><FONT SIZE=3D2>&gt; &gt;Troy Sorzano</FONT>
<BR><FONT SIZE=3D2>&gt; &gt;Info Packaging</FONT>
<BR><FONT SIZE=3D2>&gt; &gt;</FONT>
</P>

<P><FONT FACE=3D"Arial" SIZE=3D2 COLOR=3D"#000000"></FONT>&nbsp;

</BODY>
</HTML>
------_=_NextPart_001_01C309BD.251FB610--

------_=_NextPart_000_01C309BD.251FB610
Content-Type: application/octet-stream;
	name="wrappedpie.py"
Content-Transfer-Encoding: quoted-printable
Content-Disposition: attachment;
	filename="wrappedpie.py"

from math import sin, cos, pi
from reportlab.graphics.charts.piecharts import Pie
from reportlab.graphics.shapes import Ellipse, Group, Line, Rect, =
String, Wedge
from reportlab.lib import colors
from reportlab.lib.attrmap import AttrMap, AttrMapValue
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.validators import isNumber, isBoolean
from reportlab.platypus import Paragraph

class WrappedPie(Pie):
    """
    A pie chart class, derived from the standard ReportLab Pie chart =
class,
    with wrapping for text labels. Text labels can be wrapped around =
the chart itself
    and within a vertical column of specified width.
    Marc Louis Stober, mstober@dalbar.com, rev. April 23, 2003.

    Contains two attributes in addition to those in the standard Pie =
class:
    .columnWidth: width of column in which to center pie and wrap the =
text.
    .showGuides: for debugging, to show how text boxes are sized and =
positioned.

    Also:
    .slices.labelRadius: works differently; the label will be to the =
outside of the
        radius, rather than centered.

    How the wrapping works:
    For each slice, the algorithm attempts to fit the label within a =
vertical
    column defined by the columnWidth attribute, and to the left or =
right of
    the pie slice.

    How close the label is to the pie slice depends on the value of
    labelRadius, with a value of 1 theoretically putting the edge of
    the label on the edge of the slice.

    The ReportLab Paragraph class is used for its wrapping alogrithm.
    Finally, the label may be adjusted downward to prevent overlapping
    with other labels.
    """

    _attrMap =3D AttrMap(Pie._attrMap,
            columnWidth =3D AttrMapValue(isNumber, desc=3D'Width of =
column in which to contain chart and labels.'),
            showGuides =3D AttrMapValue(isBoolean, desc=3D'Show guide =
lines for debugging text wrapping.')               =20
        )

    def __init__(self):
        Pie.__init__(self)
        self.columnWidth =3D 0
        self.showGuides =3D False

    def makeWedges(self):
        # normalize slice data
        normData =3D self.normalizeData()
        n =3D len(normData)

        # set default column width =3D 2 * width (if not specified)
        if self.columnWidth =3D=3D 0: self.columnWidth =3D self.width * =
2

        # labels
        if self.labels is None:
            labels =3D [''] * n
        else:
            labels =3D 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 =3D n-len(labels)
            if i>0:
                labels =3D labels + ['']*i

        xradius =3D self.width/2.0
        yradius =3D self.height/2.0
        centerx =3D self.x + xradius
        centery =3D self.y + yradius

        if self.direction =3D=3D "anticlockwise":
            whichWay =3D 1
        else:
            whichWay =3D -1

        g =3D Group()
        labels2 =3D []
        i =3D 0
        styleCount =3D len(self.slices)

        startAngle =3D self.startAngle #% 360
        for angle in normData:
            endAngle =3D (startAngle + (angle * whichWay))
            if abs(startAngle-endAngle)>=3D1e-5:
                if startAngle < endAngle:
                    a1 =3D startAngle
                    a2 =3D endAngle
                else:
                    a1 =3D endAngle
                    a2 =3D startAngle

                #if we didn't use %stylecount here we'd end up with the =
later wedges
                #all having the default style
                wedgeStyle =3D self.slices[i%styleCount]
                # is it a popout?
                cx, cy =3D centerx, centery
                if wedgeStyle.popout <> 0:
                    # pop out the wedge
                    averageAngle =3D (a1+a2)/2.0
                    aveAngleRadians =3D averageAngle * pi/180.0
                    popdistance =3D wedgeStyle.popout
                    cx =3D centerx + popdistance * cos(aveAngleRadians)
                    cy =3D centery + popdistance * sin(aveAngleRadians)

                if n > 1:
                    theWedge =3D Wedge(cx, cy, xradius, a1, a2, =
yradius=3Dyradius)
                elif n=3D=3D1:
                    theWedge =3D Ellipse(cx, cy, xradius, yradius)

                theWedge.fillColor =3D wedgeStyle.fillColor
                theWedge.strokeColor =3D wedgeStyle.strokeColor
                theWedge.strokeWidth =3D wedgeStyle.strokeWidth
                theWedge.strokeDashArray =3D wedgeStyle.strokeDashArray

                g.add(theWedge)

                # now draw a label
                if labels[i] <> "":
                    averageAngle =3D (a1+a2)/2.0
                    aveAngleRadians =3D averageAngle*pi/180.0
                    labelRadius =3D wedgeStyle.labelRadius
                    labelX =3D centerx + (0.5 * self.width * =
cos(aveAngleRadians) * labelRadius)
                    labelY =3D centery + (0.5 * self.height * =
sin(aveAngleRadians) * labelRadius)

                    pos =3D int(len(labels[i])/2)
                    while str(labels[i])[pos] !=3D ' ' and pos < =
len(labels[i]):
                        pos +=3D 1

                    # use a paragraph to wrap the text. since we can't =
actually draw a paragraph
                    # ... in a RL drawing, we'll just take the split =
text out below
                    ps =3D ParagraphStyle('PieLabel')
                    ps.fontName =3D wedgeStyle.fontName
                    ps.fontSize =3D wedgeStyle.fontSize
                    ps.fontColor =3D wedgeStyle.fontColor
                    ps.leading =3D wedgeStyle.fontSize * 1.2
                    p =3D Paragraph(labels[i], ps)
                    relativeBoxSize =3D self.columnWidth / self.width
                    if labelX > centerx:
                        w =3D (centerx * relativeBoxSize) - labelX
                        if self.showGuides:
                            labels2.append(Line(centerx * =
relativeBoxSize, labelY, labelX, labelY, strokeColor =3D =
colors.lightcyan))
                    else:
                        w =3D labelX
                        if self.showGuides:
                            labels2.append(Line(0, labelY, labelX, =
labelY, strokeColor =3D colors.lightcyan))
                    w, h =3D p.wrap(w, 9999)
                    # figure out minimum width needed
                    minW =3D 0
                    for linelist in p.blPara.lines:
                        minW =3D max(minW, w - linelist[0])
                    w =3D minW
                    yOffsetRatio =3D (abs(averageAngle + 90) % 90) / 90
                    yOffsetRatio =3D abs((averageAngle % 180 - 2 * ( =
averageAngle % 90 )) / 90)
                    if labelY > centery:
                        labelY1 =3D labelY - averageAngle % 90 / 90 * h =
/ 2.
                    else:
                        yOffsetRatio *=3D -1 # offset labels down on =
the bottom
                        labelY1 =3D labelY - (1.0 - averageAngle % 90 / =
90) * h / 2.
                    labelY1 =3D labelY - h / 2
                    labelY1 +=3D yOffsetRatio * h / 2
                    if labelX > centerx:
                        side =3D 'R'
                        labelX1 =3D labelX
                        if self.showGuides:
                            labels2.append(Rect(labelX, labelY1, w, h, =
fillColor =3D None, strokeColor =3D colors.lightcyan))
                    else:
                        side =3D 'L'
                        labelX1 =3D labelX - w
                        if self.showGuides:
                            labels2.append(Rect(labelX - w, labelY1, w, =
h, fillColor =3D None, strokeColor =3D colors.lightcyan))

                    # reset current Y position when we move to the =
other size
                    if getattr(self, '_lastSide', None) !=3D side:
                        self._currentY =3D labelY1 + h
                    self._lastSide =3D side
                   =20
                    # starting position below any previous labels
                    if side =3D=3D 'R':
                        self._currentY =3D min(labelY1 + h, =
self._currentY)
                    else:
                        self._currentY =3D max(labelY1 + h, =
self._currentY)
                    for linelist in p.blPara.lines:
                        self._currentY -=3D ps.leading
                        linelist =3D linelist[1]
                        line =3D ''
                        for word in linelist:
                            line +=3D '%s ' % word
                        line =3D line.rstrip()
                        theLabel =3D String(labelX1 + w / 2, =
self._currentY, line)
                        theLabel.textAnchor =3D "middle"
                        theLabel.fontSize =3D wedgeStyle.fontSize
                        theLabel.fontName =3D wedgeStyle.fontName
                        theLabel.fillColor =3D wedgeStyle.fontColor
                        labels2.append(theLabel)
                       =20
            startAngle =3D endAngle
            i =3D i + 1

        # add the labels at the end so they print over wedges [MLS =
9/30/02]
        for label in labels2:
            g.add(label)

        return g

if __name__ =3D=3D '__main__':

    from reportlab.graphics import renderPDF
    from reportlab.graphics.shapes import Drawing
    import sys
   =20
    # create a drawing on which to draw the chart
    d =3D Drawing(500, 500)

    # create a pie chart
    p =3D WrappedPie()

    # add chart to drawing
    d.add(p)

    # set data and properties of chart
    p.x =3D 100
    p.y =3D 100
    p.width =3D 300
    p.height =3D 300
    p.slices.labelRadius =3D 1.1
    p.showGuides =3D True # for debugging
    p.columnWidth =3D 600=20
    p.data =3D [22,
            19.4,
            15.714,
            15.463,
            15,
            15,
            14.6,
            14,
            13.417,
            13.35]
    p.labels =3D ('Alex Rodriguez, Shortstop - Texas $22.000 million',=20
            'Carlos Delgado, First Base - Toronto $19.400 million',
            'Kevin Brown, Pitcher - Los Angeles $15.714 million',
            'Manny Ramirez, Left Field - Boston $15.463 million',
            'Sammy Sosa, Right Field - Chicago-NL $15.000 million',
            'Barry Bonds, Right Field - San Francisco $15.000 million',
            'Derek Jeter, Shortstop - NY Yankees $14.600 million',
            'Pedro Martinez, Pitcher - Boston $14.000 million',
            'Shawn Green, Right Field - Los Angeles $13.417 million',
            'Randy Johnson, Pitcher - Arizona $13.350 million')

    # add some labels
    d.add(String(250, 450, 'Top 10 MLB Salaries',
                 textAnchor =3D 'middle', fontSize =3D 18))
    d.add(String(30, 30, 'Source: Infoplease.com',
                 fontSize =3D 8))


    # save as (name of script).pdf
    filename =3D '%spdf' % sys.argv[0][:-2]
    renderPDF.drawToFile(d, filename)
    print "Pie Chart Demo saved to '%s'." % filename

   =20


------_=_NextPart_000_01C309BD.251FB610--