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

Drawing the Schedule

Now we've come to the heart of the problem: drawing the actual schedule. This brings up two important questions. First, how will we represent the data? And, second, how will we code the logic to display it? A good answer to the first question will make the second question easier, so let's start there.

Examining our chart, we can guess that our data structure will require at least the following:

When we look at the relationship between these elements, we see:

Also note that the start and end times don't necessarily coincide with our columns. To keep things simple, we'll divide each month into four equal chunks rather than into weeks or days. So a time of 3 represents the third quarter-month or (given our labels) three fourths of the way through April. We'll also set a variable called now to indicate the current time, which lets us show progress by coloring in the past.

There are many ways to translate all this into Python, but I chose to represent it with a combination of lists, dictionaries, and tuples. I feel this makes the structure easier to read than the other representations I tried.

tasks = [
  {"label": "zikeshop",       "boxes": [(3,   8,  "cornflower"),
                                        (12, 16,  "cornflower")]},
  {"label": "blogdrive",      "boxes": [(8,  12,  "cornflower")]},
  {"label": "day job",        "boxes": [(0,   4,  "red"),
                                        (16,  30, "red")]},
  {"label": "body-for-life",  "boxes": [(4,  16,  "gold"),
                                        (18, 30,  "gold")]},
  {"label": "writing",        "boxes": [(2,   4,  "limegreen"),
                                        (6,  24,  "limegreen")]},
  {"label": "linkwatcher",    "boxes": [(4,   6,  "limegreen")]},
now = 3

Because each dictionary represents a row, and each boxes value represents the boxes on that row, we can draw the whole thing with a pair of nested loops. The outer loop moves us downward, while the inner loop draws the boxes across the chart.

We need some more math to calculate the exact positions of the boxes, but it's simple. For left and right coordinates, we multiply the time (measured in quarter cell-widths) by (cellW / 4) and add the width of the left column. For top and bottom coordinates, we only have to look at cellH and topRowH.

Here's the code:

## draw "past" shading first, so it goes on bottom:
c.drawRect(lftColW,topRowH, lftColW + (now * (cellW/4)), imgH,, fillColor=pid.slategray)

## draw the charts:
for i in range(len(tasks)):

  # draw label left-aligned and approximately 
  # centered vertically
               5,                          # left
               topRowH + 15 + (i * cellH), # top

 # draw the boxes
 for box in tasks[i]["boxes"]:
   begin, end, color = box
   c.drawRect(lftColW + ((cellW / 4) * begin), # left
              topRowH + (i * cellH),           # top
              lftColW + ((cellW / 4) * end),   # right
              topRowH + ((i+1) * cellH),       # bottom
              fillColor=getattr(pid, color, pid.white))

One last thing. If you run this code, you'll notice that some of the boxes extend past the right edge of the chart, and thus overwrite the right hand border. The proper time to draw the outside border is at the end of the script, after we've drawn any runaway boxes.

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

Where to Next?

Now you can draw simple graphics with piddle. There's plenty more to piddle that we haven't touched on. The piddle reference manual and the examples on the piddle home page can give you some ideas of what else piddle can do. For the latest news and discussions, check out the pythonpiddle mailing list.

There's obviously a lot more we could add to the Gantt chart script. A real Gantt chart would likely have some sort of key to label the different colors, and would probably provide a more accurate timeline. It would also be nice to have a user-friendly interface for specifying the schedule and the ability to store multiple schedules in external files or a database. In fact, next time, we'll write a simple CGI interface to build a schedule and generate Gantt charts in real time on the Web.

Michal Wallace is a web developer and entrepreneur in Atlanta, Georgia.

Return to the Python DevCenter.

Copyright © 2017 O'Reilly Media, Inc.