XML.com: XML From the Inside Out
oreilly.comSafari Bookshelf.Conferences.

advertisement

A Bright, Shiny Service: Sparklines

June 22, 2005

I really was working on the bookmark web service—really, I was!—but I got distracted. What grabbed my attention was Sparklines.

What are Sparklines?

Sparklines, as defined by Edward Tufte, are intense, simple word-sized graphics. They are small graphics embedded within a context of words or numbers. They are described in a chapter, which he's published on the Web, of his soon to be released book Beautiful Evidence. That article has been up for a while, but looking at the number of new links to it per month as recorded by Technorati 0 6 you can see that interest is ramping up. That little graph showing the links per month is a sparkline.

The BitWorking Sparkline Generator is my contribution to the Web 2.0: it's a web service, web application, and the source code to both. It's also the subject of this article.

Drawing Sparklines in Python

Sparklines are useful for presenting a large volume of information in a small space using a context sensitive manner. I have test routines that I run regularly and the volume of output can be tremendous. I stumbled onto sparklines and found they are a great way to ease the information overload. I started drawing sparklines in Python using the Python Imaging Library. It is easy to get started with a basic template and then modify the code. Here are some examples:

import Image, ImageDraw
import StringIO

def plot_sparkline(results):
  """Returns a sparkline image as a data: URI.
     The source data is a list of values between
     0 and 100. Values greater than 95
     are displayed in red, otherwise they are displayed
     in green"""

  im = Image.new("RGB", (len(results)*2, 15), 'white')
  draw = ImageDraw.Draw(im)
  for (r, i) in zip(results, range(0, len(results)*2, 2)):
      color = (r > 50) and "red" or "gray"
      draw.line((i, im.size[1]-r/10-4, i, (im.size[1]-r/10)), fill=color)
  del draw

  f = StringIO.StringIO()
  im.save(f, .gif")
  return f.getvalue()
        

This will produce a sparkline that looks like this:

This is just one kind of plot; it's not that much work to create a different kind of sparkline, for example, one of the continuous plots:

The code for the image above is the following:

def plot_sparkline2(results):
   im = Image.new("RGB", (len(results)+2, 20), 'white')
   draw = ImageDraw.Draw(im)
   coords = zip(range(len(results)), [15 - y/10 for y in results])
   draw.line(coords, fill="#888888")
   end = coords[-1]
   draw.rectangle([end[0]-1, end[1]-1, end[0]+1, end[1]+1], fill="#FF0000")
   del draw 

   f = StringIO.StringIO()
   im.save(f, .gif")
   return f.getvalue()

A note about the limitations of what we can do. We aren't going to reproduce Galileo's drawings of the moons of Jupiter, nor are we going to get the resolution that can be achieved on paper.

The Discovery of the Galilean Satellites

On the other hand, we can exploit the advantages of the platform of our choice. We can put info or raw data into the "title" attribute of the image. Putting the raw data into the "title" attribute of the image causes the raw data to be displayed when the mouse hovers over the image. Here is how it looks in FireFox:

A popup showing the raw data that informs a sparkline.

We can also make the sparkline clickable, either making the entire image a link, or using an image map to make parts of the sparkline lead to further resources.

Spreading the Joy via Web Services

Now hacking Python is fun, but it's not for everyone. Let's open this up for everyone to use. First, we'll start by creating a web service. What else did you expect me to do?

For simple sparklines we can use query parameters to pass the data into a CGI application that draws the sparkline. Let's start by reviewing with the four questions we ask when building any web service:

  1. What are the resources? The resources are sparklines. To specify how each sparkline will appear we can pass in the data via query parameters. Our sparkline code only takes one parameter, a list of data with values between 0 and 100. That data can be passed in by a query parameter d whose value is a comma separated list of values between 0 and 100. For example: http://bitworking.org/projects/sparklines/spark.cgi?d=10,20,30,40.
  2. What are their representations? The representations can be in an image format:.gif, GIF, JPEG or maybe even SVG.
  3. What methods do those resources support? GET
  4. What errors could be generated? 4XX if the parameters passed in don't correspond to data that can be graphed.

Now remember our follow-up questions about GETs. Is our use of GETs both safe and idempotent? Retrieving an image is certainly safe, and doing so multiple times still returns the same image, so we are using GET correctly.

Here is a first pass at an implementation of our web service. Note that this is not the service I deployed, it is a much simpler version used here just for exposition:

#!/usr/bin/env python
import cgi 
import cgitb 
import sys 
import os 
 
cgitb.enable() 
 
import Image, ImageDraw 
import StringIO 
import urllib 
 
 
def plot_sparkline(f, results): 
  """Returns a sparkline image as a data: URI.
     The source data is a list of values between
     0 and 100. Values greater than 95
     are displayed in red, otherwise they are displayed
     in green"""
  im = Image.new("RGB", (len(results)*2, 15), 'white')
  draw = ImageDraw.Draw(im)
  for (r, i) in zip(results, range(0, len(results)*2, 2)):
     color = (r > 50) and "red" or "gray"
     draw.line((i, im.size[1]-r/10-4, i, (im.size[1]-r/10)), fill=color)
  del draw
 
  im.save(f, .gif")
 
def plot_error(f): 
  im = Image.new("RGB", (40, 15), 'white')
  draw = ImageDraw.Draw(im)
  draw.line((0, 0) + im.size, fill="red")
  draw.line((0, im.size[1], im.size[0], 0), fill="red")
  del draw
  im.save(f, .gif")
 
def error(status="Status: 400 Bad Request"): 
   print "Content-type: image.gif"
   print status
   print ""
   plot_error(sys.stdout)
   sys.exit()
 
def cgi_param(form, name, default): 
   return form.has_key(name) and form[name].value or default
 
if not os.environ['REQUEST_METHOD'] in ['GET', 'HEAD']: 
   error("Status: 405 Method Not Allowed")
form = cgi.FieldStorage() 
raw_data = cgi_param(form, 'd', '') 
if not raw_data: 
   error()
data = [int(d) for d in raw_data.split(",") if d] 
if min(data) < 0 or max(data) > 100: 
   error()
 
print "Content-type: image.gif" 
print "Status: 200 Ok" 
print "" 
plot_sparkline(sys.stdout, data)         

There are a few noteworthy points:

Errors
If some of the parameters are missing, or incorrect, we return an error message that is the same type as a successful response, that is, we return a big red X as a .gif to indicate that there was an error. That's because our service will be used to serve up images that will most likely appear in web pages via the <img/> element. This way if an error occurs there will be visible feedback by the appearance of a large red X.
Methods
Note that we manually restrict our handling of HTTP methods to those of just GET and HEAD. If we don't do this, then our web service will also respond to POST methods. That's because our query parameter parsing library is a little too helpful and will handle POSTed data in an indistinguishable manner from GET requests. In this case it's not really that damaging, but imagine if the tables had been turned and we had created a service that should only respond to POST. Unless we check the incoming method ourselves then our service would gleefully accept both GET and POST requests and treat them as the same. That can lead to ugly problems, particularly if we settled on using POST because the action taken wasn't idempotent or safe.

Full Web Service Description

Here is a full description of the web service as it is deployed today:

            http://bitworking.org/projects/sparklines/spark.cgi
        
Common Parameters
Parameter Description
d The data for the plot. All data values must be between 0 and 100.
height The height of the image in pixels.
type "discrete" - One vertical bar per data point.
"smooth" - all the points plotted as a continuous line.

If the type is "smooth" then the following parameters apply:

"Smooth" Parameters
Parameter Description
min-m If set to 'true', then place a special marker at the smallest value in the data set.
max-m If set to 'true', then place a special marker at the largest value in the data set.
last-m If set to 'true', then place a special marker at the last value in the data set.
min-color The color of the marker placed at the smallest value in the data set.
max-color The color of the marker placed at the largest value in the data set.
last-color The color of the marker placed at the last value in the data set.
step The points are to be plotted every n'th pixel.

If the type is discrete then the following parameters apply:

"Discrete" Parameters
Parameter Description
upper Data values ≥ upper will be plotted in the above-color, otherwise data points will be plotted in the below-color.
above-color The color for data points ≥ upper.
below-color The color for data points < upper.

Here are some example sparklines and their URIs to get you started.

Examples
Sparkline URI
http://bitworking.org/projects/sparklines/spark.cgi? type=smooth&d=10,20,30,90,80,70&step=4
http://bitworking.org/projects/sparklines/spark.cgi? type=smooth&d=10,20,30,90,80,70&step=4&min-m=true&max-m=true
http://bitworking.org/projects/sparklines/spark.cgi? type=smooth&d=10,20,30,90,80,70

Pages: 1, 2

Next Pagearrow







close