A Bright, Shiny Service: Sparklines
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-r/10-4, i, (im.size-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-1, end-1, end+1, end+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.
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:
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:
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
dwhose value is a comma separated list of values between 0 and 100. For example:
- What are their representations? The representations can be in an image format:.gif, GIF, JPEG or maybe even SVG.
- What methods do those resources support? GET
- 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-r/10-4, i, (im.size-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, im.size, 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:
- 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.
- 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:
|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:
|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:
|upper||Data values ≥ upper will be plotted in the
|above-color||The color for data points ≥ |
|below-color||The color for data points < |
Here are some example sparklines and their URIs to get you started.
Pages: 1, 2