Code


Version 8 (modified by programmerq, 6 years ago) (diff)

not a traclink

Creating bitmap charts in Django with the ReportLab library

This describes a simple way to create dynamic bitmap charts in Django.

ReportLab's PDF library is well known, but less people are aware that it contains a mature graphics package able to create most kinds of business charts, as well as custom graphical widgets. We've been serving GIF and PNG charts for many years. Doing this within Django has turned out to be trivial; here's an example.

Installation

You will need the ReportLab library from http://www.reportlab.org/

Also make sure you have compiled the _rl_accel and _renderPM packages. The latter is a sophisticated bitmap renderer with anti-aliasing, font handling and so on. ReportLab's download page has a checker script to tell you what extensions are installed.

Creating a Horizontal Bar chart

The reportlab/graphics framework provides a Drawing object and many Shape objects, which include primitive shapes, and high-level objects like charts and legends. You can easily write your own too if you want to make dynamic bitmap buttons or fancy dashboard apps.

The usual pattern we follow is to create a Drawing class with all of your widgets, some sensible default data, and any visual settings you want. Here is such a class, which has a bar chart and a string for the chart title.

#    mycharts.py  
from reportlab.graphics.shapes import Drawing, String
from reportlab.graphics.charts.barcharts import HorizontalBarChart

class MyBarChartDrawing(Drawing):
    def __init__(self, width=400, height=200, *args, **kw):
        apply(Drawing.__init__,(self,width,height)+args,kw)
        self.add(HorizontalBarChart(), name='chart')

        self.add(String(200,180,'Hello World'), name='title')

        #set any shapes, fonts, colors you want here.  We'll just
        #set a title font and place the chart within the drawing
        self.chart.x = 20
        self.chart.y = 20
        self.chart.width = self.width - 20
        self.chart.height = self.height - 40

        self.title.fontName = 'Helvetica-Bold'
        self.title.fontSize = 12
        
        self.chart.data = [[100,150,200,235]]
        

if __name__=='__main__':
    #use the standard 'save' method to save barchart.gif, barchart.pdf etc
    #for quick feedback while working.
    MyBarChartDrawing().save(formats=['gif','png','jpg','pdf'],outDir='.',fnRoot='barchart')

Paste this into a file and execute it; you should get 4 chart files written to disk in different formats.

Integrating into Django

Now we add a view to our views.py. This will examine the request for any dynamic parameters, since there's not much point serving a chart that doesn't vary. We'll allow the user to pass in 4 things as GET or POST parameters: a title, a comma-separated list of numbers, and the overall width and height of the image. Everything has a default in our Drawing class anyway, so we only pass through parameters which are present.

You then ask the Drawing to render itself to your favourite bitmap format and generate a response with the right content-type

def barchart(request):

    #instantiate a drawing object
    import mycharts
    d = mycharts.MyBarChartDrawing()

    #extract the request params of interest.
    #I suggest having a default for everything.
    if request.has_key('height'):
        d.height = int(request['height'])
    if request.has_key('width'):
        d.width = int(request['width'])
    
    if request.has_key('numbers'):
        strNumbers = request['numbers']
        numbers = map(int, strNumbers.split(','))    
        d.chart.data = [numbers]   #bar charts take a list-of-lists for data

    if request.has_key('title'):
        d.title.text = request['title']
  

    #get a GIF (or PNG, JPG, or whatever)
    binaryStuff = d.asString('gif')
    return HttpResponse(binaryStuff, 'image/gif')

Finally, you need a URL mapping. In this case I have added this:

    (r'^charts/bar/$', 'myapp.views.barchart'),

Now you can start Django, point your browser at the URL with no arguments, and should see the chart. Then try out a few parameters such as

http://localhost:8000/charts/bar?title=awesome

and watch it redraw.

Creating a Line chart

#    mycharts.py  
from reportlab.graphics.shapes import Drawing, String
from reportlab.graphics.charts.lineplots import LinePlot
from reportlab.graphics.charts.lineplots import ScatterPlot
from reportlab.lib import colors
from reportlab.graphics.charts.legends import Legend
from reportlab.graphics.charts.textlabels import Label
from reportlab.graphics.widgets.markers import makeMarker


class MyLineChartDrawing(Drawing):
    def __init__(self, width=600, height=400, *args, **kw):
        apply(Drawing.__init__,(self,width,height)+args,kw)
        self.add(LinePlot(), name='chart')

        self.add(String(200,180,'Hello World'), name='title')

        #set any shapes, fonts, colors you want here.  We'll just
        #set a title font and place the chart within the drawing.
        #pick colors for all the lines, do as many as you need
        self.chart.x = 20
        self.chart.y = 30
        self.chart.width = self.width - 100
        self.chart.height = self.height - 75
        self.chart.lines[0].strokeColor = colors.blue
        self.chart.lines[1].strokeColor = colors.green
        self.chart.lines[2].strokeColor = colors.yellow
        self.chart.lines[3].strokeColor = colors.red
        self.chart.lines[4].strokeColor = colors.black
        self.chart.lines[5].strokeColor = colors.orange
        self.chart.lines[6].strokeColor = colors.cyan
        self.chart.lines[7].strokeColor = colors.magenta
        self.chart.lines[8].strokeColor = colors.brown
	
	self.chart.fillColor = colors.white
	self.title.fontName = 'Times-Roman'
	self.title.fontSize = 18
	self.chart.data = [((0, 50), (100,100), (200,200), (250,210), (300,300), (400,500))]
	self.chart.xValueAxis.labels.fontSize = 12
	self.chart.xValueAxis.forceZero = 0
	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 = 'Times-Roman'
	self.chart.yValueAxis.labels.fontSize = 12
	self.title.x = self.width/2
	self.title.y = 0
	self.title.textAnchor ='middle'
	self.add(Legend(),name='Legend')
	self.Legend.fontName = 'Times-Roman'
	self.Legend.fontSize = 12
	self.Legend.x = self.width
	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(Label(),name='XLabel')
	self.XLabel.fontName = 'Times-Roman'
	self.XLabel.fontSize = 12
	self.XLabel.x = 85
	self.XLabel.y = 5
	self.XLabel.textAnchor ='middle'
	#self.XLabel.height = 20
	self.XLabel._text = ""
	self.add(Label(),name='YLabel')
	self.YLabel.fontName = 'Times-Roman'
	self.YLabel.fontSize = 12
	self.YLabel.x = 2
	self.YLabel.y = 80
	self.YLabel.angle = 90
	self.YLabel.textAnchor ='middle'
	self.YLabel._text = ""
	self.chart.yValueAxis.forceZero = 1
	self.chart.xValueAxis.forceZero = 1

Integrating into Django

Now we add a view to our views.py.

def linechart(request):

    #instantiate a drawing object
    d = MyLineChartDrawing()

    #extract the request params of interest.
    #I suggest having a default for everything.
    

    d.height = 300
    d.chart.height = 300
    

    d.width = 300
    d.chart.width = 300
   
    d.title.text = request.session.get('Some custom title')
    


    d.XLabel._text = request.session.get('X Axis Labell')
    d.YLabel._text = request.session.get('Y Axis Label')

    d.chart.data = [((1,1), (2,2), (2.5,1), (3,3), (4,5)),((1,2), (2,3), (2.5,2), (3.5,5), (4,6))]
   

    
    labels =  ["Label One","Label Two"]
    if labels:
        cnt=0
        # set colors in the legend
        d.Legend.colorNamePairs = []
        for label in labels:
                d.Legend.colorNamePairs.append((d.chart.lines[cnt].strokeColor,label))
                cnt=cnt+1


    #get a GIF (or PNG, JPG, or whatever)
    binaryStuff = d.asString('gif')
    return HttpResponse(binaryStuff, 'image/gif')

Finally, you need a URL mapping. In this case I have added this:

    (r'^charts/line/$', 'myapp.views.linechart'),

Now you can start Django, point your browser at the URL with no arguments, and should see the chart. Then try out a few parameters such as

http://localhost:8000/charts/line?title=awesome

and watch it redraw.

Tips

The above approach will let you drive a chart by generating img tags with all parameters embedded in them. This will also let the world use you as a chart server, so you might want to implement some authentication on the chart requests.

Very often you are plotting data which mostly exists on the server side. In this case it's inefficient to encode it all in a long URL in the HTML then have this posted back to make the chart. For example, if you run an ISP and are plotting server uptime statistics, presumably the URL or parameters would encode the customer and server ID, and you'd look up all the numeric data or that chart in your view code in a database query.

If you cannot always create a sensible chart from the input, it may be a good idea to create an error handler which returns a rectangular drawing with an error message, because this will usually be called inside an <img> tag and the browser won't display any HTML error messages if it was expecting an image.

Disclaimer

The author is pretty new to Django and if there are nicer ways to do this I would welcome feedback...

Learning more about the chart library

The available chart types, widgets and properties are covered in the two Graphics manuals on this page. Enjoy!

http://www.reportlab.org/os_documentation.html

If you have questions about the charts rather than the Django integration, ask on the reportlab-users list:

http://two.pairlist.net/mailman/listinfo/reportlab-users