The goal of this lab is to provide a gentle introduction to Processing, one of the environments we will use for this class. It will also help you get used to drawing to the screen, loading data from a file, and thinking about how to represent data.
What is Processing?
Processing is actually several things:
- A website.
- A programming environment focused on learning computation design.
- A sketchbook for rapid prototyping.
- A 2D or 3D graphics API & rendering engine for Java.
- An open project created by Casey Reas and Ben Fry.
It is especially useful in this course because it is straight-forward to learn and is well-suited to graphics applications. Processing has several language modes, including Java and Python. We’re going to use the Python mode, but there is also a Java version of this lab available. Processing provides a basic set of drawing operations that simplifies the task of setting up a drawing canvas and drawing on it.
It is great for generating and modifying images, and includes support for:
- vector & raster drawing,
- image processing,
- color models,
- mouse and keyboard events,
- network communication,
- object-oriented programming,
- and lots more.
In short, it has what we need to be able to create complex applications, while letting us focus on the representation and interaction in our visualizations.
by Philipp Steinweber & Andreas Koller
Travel Time Tube Map
by Tom Carden
by Ben Fry (Cover of Nature)
Processing includes its own development environment, shown below:
The Processing development environment refers to projects as sketches (which is related to its origins as a “sketchbook” for rapid prototyping). A Sketch can be made up of several files, which can be
.pde (processing development environment) or
In this assignment, we are going to learn how to use Processing to draw the following image:
In order to do so, you will need to:
- Load and parse the data.
- Choose an internal data structure to represent these data.
- Draw them to the screen.
The data we will use comes from www.galichon.com/codesgeo. The original data was in the form:
- (name, postal code, insee code, longitude, latitude) - (name, insee code, population, density)
- (postal code, x, y, insee code, place, population, density)
First, open up Processing. In the lab, you can use the command-line by opening a Terminal window and typing:
If you are using your own computer, you can download Processing.
On the top, right corner of the window, you should see a pull-down menu with the word
Java in it. Processing is currently in Java mode. Switch it to Python mode. You should see Processing restart automatically. If you are using your own machine, you will need to download Python mode with the
Add modes... option in this menu.
Let’s create a blank canvas. In this lab, I’ll start off by showing you the code to write. As we progress, I’ll show you less and less, and leave more open to your own interpretation. Please resist the temptation to just copy and paste the code. It’s important to understand what’s going on. Even if it seems obvious, writing (or typing) the code out by hand helps to reinforce things in your mind, so please type the following code in the window:
draw() methods are built-in methods in Processing that are called automatically when your program starts and each time the window repaints, respectively.
Try pressing the
Run button. You should see an 800 by 800-pixel-wide window with a white (255) background.
Now lets take a look at the data we’re going to use. Open them in your preferred text editor. Notice that this a file containing a data table in a tab-separated format. Let’s go ahead and read that file:
setup(). When you run the program, you should see this data file displayed on the console.
Parsing the data
Now let’s parse the data so we can actually make use of it. For now, we’re going to break good coding practice and define a few global variables to store the coordinates of all of the places in our data file. At the top of the file, add the following lines:
We’ll store the coordinates of all of the places in the data file in the
places array. We’ll come back to the
minX, maxX and
Y later. Add to the top of
readData() the line
global minX, maxX, minY, maxY and the following lines at the end:
# First line contains metadata # Second line contains column labels # Third line and onward contains data cases for line in lines[2:]: columns = line.split("\t") longitude = float(columns) latitude = float(columns) places.append( (longitude, latitude) ) minX = min(places, key=lambda place: place) maxX = max(places, key=lambda place: place) minY = min(places, key=lambda place: place) maxY = max(places, key=lambda place: place)
Try running the program. You won’t see anything new, but it should at least run without errors. It’s usually a good idea to frequently run your program while you’re developing it to make sure you haven’t introduced any new bugs, even if you won’t necessarily see anything new.
Now let’s get to the fun part. We’ve extracted the points from the data file and stored them in our x/y arrays. Let’s see what this looks like. Update the
draw() method to add:
If we run our program, we should now see… an exception, something about NaN when converting to an int. If we take a closer look at our data, we’ll find out that it’s actually kind of messy. Among other messiness, some of our places have invalid positions. Let’s modify our drawing loop to ignore these places:
What do you expect to happen when you run the program? Try it out. Run the program. What actually happens? Can you think of a reason why we don’t see anything on the screen?
Still not sure? Let’s take a closer look at our data. Recall that each row is in the following format:
(postal code, x, y, insee code, place, population, density)
We only use the
y columns. Think about it before continuing on.
y columns in the data set are expressed in longitude and latitude, not in terms of pixel coordinates on the screen. All of our coordinates correspond to a single pixel, which happens to be drawn off-screen since all of France has a negative longitude.
We need to create a mapping from longitude, latitude to x, y-coordinates on the screen. Assuming a flat projection, the math is actually pretty simple, and processing has a builtin function to do this for us:
map(value, domainMin, domainMax, rangeMin, rangeMax)
This function will take a
value in a given domain and normalize it to the given range. We already know the domain of our values; we calculated it earlier and stored it in minX, maxX, etc. Let’s modify our drawing loop to use them as follows:
height are builtin to Processing and correspondent to the dimensions of our window (what we gave to
size() in our
Let’s try running the program one more time, and this time we should be able to marvel in the wonder of your beautiful drawing. Or fix any bugs and then marvel.
Have you noticed something peculiar about our map of France?
In most graphics environments, the origin (0, 0) is located at the top, left of the canvas. Latitudes, on the other hand, start at the equator, which we tend to think of as being to the bottom of France. No worries, all we need to do is invert either our domain or our range in the above mapping function.
Make that change and re-run the program. Is Corsica at the top or the bottom? Do we indeed see the Finistère next to the Channel, or is it hanging out in the Mediterranean?
Let’s create a class
Place. We would normally create a new tab (using the right-arrow icon on the right side of the window) for our class, but there seems to be a bug in the Python version of Processing, so let’s add our class to the bottom of our main file:
We’ll then need to modify the loop in our
readData() method to use this new class:
for line in lines[2:]: columns = line.split("\t") place = Place() place.postalCode = int(columns) place.longitude = float(columns) place.latitude = float(columns) place.name = columns place.population = int(columns) place.density = float(columns) places.append(place) Place.minX = min(places, key=lambda place: place.longitude).longitude Place.maxX = max(places, key=lambda place: place.longitude).longitude Place.minY = min(places, key=lambda place: place.latitude).latitude Place.maxY = max(places, key=lambda place: place.latitude).latitude
Let’s also clean up our drawing. Instead of drawing each place in our global
draw() function, we’ll teach each place how to draw itself. Add a new method
draw(self) to your
Place class and move the code from your main
draw() function there. (Pay particular attention to the difference between each Place’s
draw(self) method, which is how each place will draw itself, and Processing’s global
draw() function, which will be called every time Processing needs to draw the canvas.
Now, in the main draw function, we only need to tell each place to draw itself. Modify the global
Try running the program. If it doesn’t run, try fixing any errors we might have introduced until it works.
It turns out that converting between longitude, latitude and pixel coordinates is something we’ll be doing a lot of in our program. Let’s refactor that out into a Python property. “Refactor” is just a fancy word for re-organizing the code we’ve written, and a Python property is basically just a method that looks like an attribute. Add the following to our
Place class (anywhere is fine, but I like to put properties after the attributes and before the methods):
Now, in our
draw(self) method, we can remove the two
map() calls and use
self.y in the
set() function call:
On your own…
That’s great for getting started, but what we’ve created so far is really only little more than an info vis “Hello, world” (or, more properly, “Hello, France!”).
To better understand our toolbox, let’s look at the Processing.py Reference. These are the builtin methods and functions of Processing. Take particular note of:
point(), line(), triangle(), quad(), rect(), ellipse(),and
background(), fill(), stroke(), noFill(), noStroke(), strokeWeight(), strokeCap(), strokeJoin(), smooth(), noSmooth(), ellipseMode(),and
You should now have the tools you need to update your visualization to:
- Show population and density
You’re also going to make your visualisation interactive. When the user clicks on a place, draw its name and postal code. You will need the following tools to do so:
Processing uses bitmap fonts to draw text. These are different than the system fonts installed. To create one, go to the
Tools > Create Font... menu. This panel will let you generate a bitmap font you can use in your program. Copy the filename that is created at the bottom of the screen. Then double-check that the font file is in the data folder of your project using the
Sketch > Show Sketch Folder menu.
To use the font, first create a global variable
labelFont to store the font. Then modify your
setup() method to load and configure the font:
Then modify your drawing method to draw some text using
text("Hello, France!", x, y) where
y are the coordinates at which to draw the text.
When the user performs certain actions, such as moving the mouse, pressing or releasing the mouse button, etc, Processing will call certain methods:
You can also access the mouse position with
mouseY, and you can test which button is pressed with:
Labs 1 and 2 are due by “midnight” on Friday, June 2nd. To turn in the lab, create a .zip or .tar.gz archive of your projects and submit them using the Moodle.
Assignment based on one by Petra Isenberg, Jean-Daniel Fekete, Pierre Dragicevic and Frédéric Vernier under a Creative Commons Attribution-ShareAlike 3.0 License.