Python DevCenter
oreilly.comSafari Books Online.Conferences.


Generating Graphics With Piddle

by Michal Wallace

You probably know by now that Python's flexible data structures make working with complex data a breeze. Did you know Python also makes it a snap to visualize your data in vibrant color?

Thanks to the slew of graphics modules people have built, Python is a handy language for creating graphics on the fly. This article looks at two of those modules and shows how to use them to generate graphics from a Python data structure. The example we'll use is a simple Gantt chart.

Gantt charts, a type of timeline often used in project planning, clearly show how resources are allocated to different tasks over time. A Gantt chart makes it easy to see who's supposed to be doing what, and when they're supposed to be finished. It also shows who's overbooked and who's free to pick up the slack.

I use Gantt charts to help organize my projects so I don't try to take on too much. In this case, the resources I am tracking are really just chunks of my time and energy. If I see less than three boxes in a column, I know I've got some free time coming up. If I see more than that, I try to reschedule things. Here's a recent example:

Gantt chart.

This chart, reduced in size to fit this page, was generated with just under 100 lines of Python code, using two imaging modules: piddle and PIL.

Piddle stands for Plug In Drawing, Does Little Else. It provides a simple interface for drawing 2D lines, arcs, shapes, and text. It allows you to write your drawings to a variety of file types and to interactive displays generated by Tk or wxWindows.

You can fetch Piddle from SourceForge. You'll notice two packages there: piddle and sping. At the moment, sping (Simple Platform Independent Graphics) is the development branch of piddle, rebundled as a python package and renamed to be a little less offputting. But we'll use piddle for this example.

We'll also need the Python Imaging Library (PIL), which we'll use to save our chart as a JPEG. The PIL homepage offers the complete source as well as precompiled binaries for Windows.

Why two modules? PIL by itself is excellent for general image manipulation. It takes care of the low level details of reading and writing file formats, scaling and rotating images, and dealing with individual pixels. While PIL provides some drawing capabilities, piddle operates at a higher level. It offers more choices for storing and displaying images, and it provides the support I mentioned for interactive drawings (though we won't get into that here).

Getting Started

To install piddle, add the unzipped piddle directory to your Python path. To install PIL on windows, unzip the precompiled binaries to your Python directory. Installing PIL on Unix is a bit trickier; you'll have to follow the installation instructions to compile it yourself.

We'll test the installation by generating a simple, empty graphic. The first step is to define a Canvas object. A piddle Canvas is a rectangular drawing area similar to a painter's canvas, but composed of pixels.

There is a different Canvas class for each piddle backend. Since we're working with PIL, we'll use piddlePIL.PILCanvas. Because we might want to change to another backend later, we'll use python 2.0's import as syntax to hide the module name:

import piddlePIL as pid
c = pid.PILCanvas(size=(250,250))

# .. code to draw graphics goes here .."gantt", format="jpeg")

If everything is installed correctly, running this code will save a completely blank 250x250 JPEG called gantt.jpeg to the current directory. Note that at the time of this writing, neither piddle nor PIL has been updated for python 2.1. If you're using the latest interpreter you may get some warning messages, however, the code should still run.

Drawing the Background

Now that we've got the basics working, let's draw the simple outline of our chart. A Gantt chart is essentially a set of rows and columns with some colored rectangles drawn on it, so we'll use the drawRect and drawLine methods from PILCanvas to do most of our work.

Before we start drawing, let's define a few variables to control how we draw: things like column titles, cell sizes, and even the size of the image itself. This will make the code a little easier to read, and also make it easier for us to tweak our image later. Replace the canvas definition with the following:

titles = ["apr","may","jun","jul","aug","sep"]

cols = len(titles) # number of data columns
rows = 6           # number of data rows
cellW = 80         # cell width
cellH = 20         # cell height
lftColW = 100      # width of left column (task names)
topRowH = 25       # height top row (month names)

# calculate image size and define the canvas:
imgW = lftColW + (cellW * cols)
imgH = topRowH + (cellH * rows)
c = pid.PILCanvas(size=(imgW,imgH))

For the chart's background, we'll draw some shaded boxes on the top and left and use a couple for loops to draw the lines. In the code below, notice how passing three arguments to Python's range function lets us define our grid wholly in terms of the variables we created. The arguments tell Python where to start, where to end, and how long to make each step in between. To avoid clutter, we'll multiply cellH by two in the second loop, skipping every other horizontal line.

## color in the headers:
c.drawRect(0,0,imgW,topRowH, fillColor=pid.gainsboro)
c.drawRect(0,0,lftColW,imgH, fillColor=pid.gainsboro)

# and the area where toprow and lftcol overlap:
c.drawRect(0,0,lftColW,topRowH, fillColor=pid.silver)

## draw the grid:
for x in range(lftColW, imgW, cellW):
  c.drawLine(x, 0, x, imgH)

for y in range(topRowH, imgH, cellH * 2):
  c.drawLine(0, y, imgW, y)

## draw outer border
c.drawRect(0, 0, imgW-1, imgH-1)

To fill in the labels, we'll call the drawString method, passing it a string, some x and y coordinates, and a Font object.

We need some math to center the labels. The formula for centering requires taking the width of the string in pixels and subtracting half from the point around which we're centering. Piddle provides the stringhWidth method for just this task:

## column titles, centered horizontally and vertically:
for i in range(cols):
               lftColW + (cellW * i) 
                 + (cellW - c.stringWidth(titles[i]))/2, # left
               (topRowH + 12)/2,                         # top

Pages: 1, 2

Next Pagearrow

Sponsored by: