Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 

Diagrams for pyFPDF (fpdf2)

Draw pie charts and bar diagrams with this script.

Tested with fpdf2 2.4.5, Pillow 8.3.1, Python 3.8.10

Example

Pie charts and bar diagrams with pyFPDF

PDF file

Usage

Just put PDFDiagrams.py in your project directory and import it.

#!/usr/bin/python3
# Ported from PHP to Python by Björn Seipel in 2021
# License: MIT
# Original Author (Sector): Maxime Delorme
# License: FPDF 
# http:#www.fpdf.org/en/script/script19.php
# Original Author (Diagrams): Pierre Marletta
# License: FPDF 

from fpdf import FPDF
from math import pi, sin, cos, floor, ceil

class PDFDiagrams(FPDF):
    legends=''
    wLegend=''
    sum=0
    NbVal=0

    def sector(self, xc, yc, r, a, b, style='FD', cw=True, o=90):
    
        d0 = a - b
        if cw:
            d = b
            b = o - a
            a = o - d
        else:
            b += o
            a += o
        
        while a<0:
            a += 360
        while a>360:
            a -= 360
        while b<0:
            b += 360
        while b>360:
            b -= 360
        if a > b:
            b += 360
        b = b/360*2*pi
        a = a/360*2*pi
        d = b - a
        if d == 0 and d0 != 0:
            d = 2*pi
        k = self.k
        hp = self.h
        if sin(d/2):
            myArc = 4/3*(1-cos(d/2))/sin(d/2)*r
        else:
            myArc = 0
        #first put the center
        self._out('%.2F %.2F m' % ((xc)*k,(hp-yc)*k))
        #put the first point
        self._out('%.2F %.2F l' % ((xc+r*cos(a))*k,((hp-(yc-r*sin(a)))*k)))
        #draw the arc
        if d < pi/2:
            self.sector_arc(xc+r*cos(a)+myArc*cos(pi/2+a),
                        yc-r*sin(a)-myArc*sin(pi/2+a),
                        xc+r*cos(b)+myArc*cos(b-pi/2),
                        yc-r*sin(b)-myArc*sin(b-pi/2),
                        xc+r*cos(b),
                        yc-r*sin(b)
                        )
        else:
            b = a + d/4
            myArc = 4/3*(1-cos(d/8))/sin(d/8)*r
            self.sector_arc(xc+r*cos(a)+myArc*cos(pi/2+a),
                        yc-r*sin(a)-myArc*sin(pi/2+a),
                        xc+r*cos(b)+myArc*cos(b-pi/2),
                        yc-r*sin(b)-myArc*sin(b-pi/2),
                        xc+r*cos(b),
                        yc-r*sin(b)
                        )
            a = b
            b = a + d/4
            self.sector_arc(xc+r*cos(a)+myArc*cos(pi/2+a),
                        yc-r*sin(a)-myArc*sin(pi/2+a),
                        xc+r*cos(b)+myArc*cos(b-pi/2),
                        yc-r*sin(b)-myArc*sin(b-pi/2),
                        xc+r*cos(b),
                        yc-r*sin(b)
                        )
            a = b
            b = a + d/4
            self.sector_arc(xc+r*cos(a)+myArc*cos(pi/2+a),
                        yc-r*sin(a)-myArc*sin(pi/2+a),
                        xc+r*cos(b)+myArc*cos(b-pi/2),
                        yc-r*sin(b)-myArc*sin(b-pi/2),
                        xc+r*cos(b),
                        yc-r*sin(b)
                        )
            a = b
            b = a + d/4
            self.sector_arc(xc+r*cos(a)+myArc*cos(pi/2+a),
                        yc-r*sin(a)-myArc*sin(pi/2+a),
                        xc+r*cos(b)+myArc*cos(b-pi/2),
                        yc-r*sin(b)-myArc*sin(b-pi/2),
                        xc+r*cos(b),
                        yc-r*sin(b)
                        )
        
        #terminate drawing
        if style=='F':
            op='f'
        elif style=='FD' or style=='DF':
            op='b'
        else:
            op='s'
        self._out(op)
    
    def sector_arc(self, x1, y1, x2, y2, x3, y3 ):
    
        h = self.h
        self._out('%.2F %.2F %.2F %.2F %.2F %.2F c' %
            (x1*self.k,
            (h-y1)*self.k,
            x2*self.k,
            (h-y2)*self.k,
            x3*self.k,
            (h-y3)*self.k))
    
    def pie_chart(self, w, h, data, format, colors=None):
    
        self.set_font('Helvetica', '', 10)
        self.set_legends(data,format)

        xPage = self.get_x()
        yPage = self.get_y()
        margin = 2
        hLegend = 5
        radius = min(w - margin * 4 - hLegend - self.wLegend, h - margin * 2)
        radius = floor(radius / 2)
        xDiag = xPage + margin + radius
        yDiag = yPage + margin + radius
        if colors == None: 
            for i in range(0,self.NbVal):
                gray = i * int(255 / self.NbVal)
                colors[i] = (gray,gray,gray)
            
        #sectors
        self.set_line_width(0.2)
        angleStart = 0
        angleEnd = 0
        i = 0
        for val in data.values(): 
            angle = (val * 360) / float(self.sum)
            if angle != 0: 
                angleEnd = angleStart + angle
                self.set_fill_color(colors[i][0],colors[i][1],colors[i][2])
                self.sector(xDiag, yDiag, radius, angleStart, angleEnd)
                angleStart += angle
            
            i+=1

        #Legends
        self.set_font('Helvetica', '', 10)
        x1 = xPage + 2 * radius + 4 * margin
        x2 = x1 + hLegend + margin
        y1 = yDiag - radius + (2 * radius - self.NbVal*(hLegend + margin)) / 2
        for i in range (0, self.NbVal):
            self.set_fill_color(colors[i][0],colors[i][1],colors[i][2])
            self.rect(x1, y1, hLegend, hLegend, 'DF')
            self.set_xy(x2,y1)
            self.cell(0,hLegend,self.legends[i])
            y1+=hLegend + margin
          
    def bar_diagram(self, w, h, data, format, color=None, maxVal=0, nbDiv=4):
    
        self.set_font('Helvetica', '', 10)
        self.set_legends(data,format)

        xPage = self.get_x()
        yPage = self.get_y()
        margin = 2
        yDiag = yPage + margin
        hDiag = floor(h - margin * 2)
        xDiag = xPage + margin * 2 + self.wLegend
        lDiag = floor(w - margin * 3 - self.wLegend)
        if color == None:
            color=(155,155,155)
        if maxVal == 0: 
            maxVal = max(data.values())
        
        valIndRepere = ceil(float(maxVal) / float(nbDiv))
        maxVal = valIndRepere * nbDiv
        lRepere = floor(lDiag / nbDiv)
        lDiag = lRepere * nbDiv
        unit = lDiag / maxVal
        hBar = floor(hDiag / (self.NbVal + 1))
        hDiag = hBar * (self.NbVal + 1)
        eBaton = floor(hBar * 80 / 100)

        self.set_line_width(0.2)
        self.rect(xDiag, yDiag, lDiag, hDiag)

        self.set_font('Helvetica', '', 10)
        self.set_fill_color(color[0],color[1],color[2])
        i=0
        for val in data.values(): 
            #Bar
            xval = xDiag
            lval = int(val * unit)
            yval = yDiag + (i + 1) * hBar - eBaton / 2
            hval = eBaton
            self.rect(xval, yval, lval, hval, 'DF')
            #Legend
            self.set_xy(0, yval)
            self.cell(xval - margin, hval, self.legends[i],0,0,'R')
            i+=1
        
        #Scales
        for i in range(0, nbDiv): 
            xpos = xDiag + lRepere * i
            self.line(xpos, yDiag, xpos, yDiag + hDiag)
            val = i * valIndRepere
            xpos = xDiag + lRepere * i - self.get_string_width(str(val)) / 2
            ypos = yDiag + hDiag - margin
            self.text(xpos, ypos, str(val))
            
    def set_legends(self, data, format):
    
        self.legends=[]
        self.wLegend=0
        self.sum=sum(data.values())
        self.NbVal=len(data)
        for l, val in zip(data.keys(),data.values()):
            p=('%.2f' % (val/self.sum*100)) + '%'
            legend=format.replace('%l',str(l)).replace('%v', str(val)).replace('%p', str(p))
            self.legends.append(legend)
            self.wLegend=max(self.get_string_width(legend),self.wLegend)

Import the script and use pyFPDF as usual.

pie_chart(float w, float h, dict data, string format [, list colors])

  • w: maximum width of the diagram.
  • h: maximum height of the diagram.
  • data: dict containing the labels and the corresponding data.
  • format: format used to display the legends. It is a string which can contain the following special values: %l (label), %v (value) and %p (percentage).
  • colors: list with lists containing the colors. If not given, some grayscale colors will be used.

bar_diagam(float w, float h, dict data, string format [, list colors [, int maxVal [, int nbDiv]]])

  • w: width of the diagram.
  • h: height of the diagram.
  • data: dict containing the labels and the corresponding data.
  • format: format used to display the legends. It is a string which can contain the following special values: %l (label), %v (value) and %p (percentage).
  • color: color of the bars. If not given, gray will be used.
  • maxVal: high value of the scale. Defaults to the maximum value of the data.
  • nbDiv: number of subdivisions of the scale (4 by default).
#!/usr/bin/python3

from PDFDiagrams import PDFDiagrams as FPDF

pdf = FPDF()
pdf.add_page()

data = {'Men':1510, 'Women':1610, 'Children':1400}

#Pie chart
pdf.set_font('Helvetica', 'BIU', 12)
pdf.cell(0, 5, '1 - Pie chart', 0, 1)
pdf.ln(8)

pdf.set_font('Helvetica', '', 10)
valX = pdf.get_x()
valY = pdf.get_y()

pdf.cell(30, 5, 'Number of men:')
pdf.cell(15, 5, str(data['Men']), 0, 0, 'R')
pdf.ln()
pdf.cell(30, 5, 'Number of women:')
pdf.cell(15, 5, str(data['Women']), 0, 0, 'R')
pdf.ln()
pdf.cell(30, 5, 'Number of children:')
pdf.cell(15, 5, str(data['Children']), 0, 0, 'R')
pdf.ln()
pdf.ln(8)

pdf.set_xy(90, valY)
col1=(100,100,255)
col2=(255,100,100)
col3=(255,255,100)
pdf.pie_chart(100, 35, data, '%l (%p)', (col1,col2,col3))
pdf.set_xy(valX, valY + 40)

#Bar diagram
pdf.set_font('Helvetica', 'BIU', 12)
pdf.cell(0, 5, '2 - Bar diagram', 0, 1)
pdf.ln(8)
valX = pdf.get_x()
valY = pdf.get_y()
pdf.bar_diagram(190, 70, data, '%l : %v (%p)', (255,175,100))
pdf.set_xy(valX, valY + 80)

pdf.output('demo.pdf')