Lab 2: Introduction to Processing (Python version)

Objectives

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.

Examples

Similar Diversity Similar Diversity
by Philipp Steinweber & Andreas Koller

Travel Time Tube Map Travel Time Tube Map
by Tom Carden

Visualizing Haplotype Visualizing Haplotype
by Ben Fry (Cover of Nature)

Getting started

Processing includes its own development environment, shown below:

Processing Development Environment

Processing Development Environment

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 .py files.

Assignment 2

In this assignment, we are going to learn how to use Processing to draw the following image:

La France

La France

In order to do so, you will need to:

  1. Load and parse the data.
  2. Choose an internal data structure to represent these data.
  3. 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)

To simplify things, we will instead use a pre-processed version of these data courtesy of Petra Isenberg, Jean-Daniel Fekete, Pierre Dragicevic and Frédéric Vernier. It merges these into:

- (postal code, x, y, insee code, place, population, density)

Download these data here

Getting started

First, open up Processing. In the lab, you can use the command-line by opening a Terminal window and typing:

~eagan/Public/processing

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:

    def setup():
        size(800, 800)
        noLoop()
    
    def draw():
        background(255)

The setup() and 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.

Loading data

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:

    def readData():
        lines = loadStrings("http://www.infres.enst.fr/~eagan/class/igr204/data/population.tsv")
        print "Loaded", len(lines), "lines" # for debugging

Now call readData() from 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:

    places = []
    minX, maxX = 0, 0
    minY, maxY = 0, 0

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[1])
        latitude = float(columns[2])
        places.append( (longitude, latitude) )
    
    minX = min(places, key=lambda place: place[0])[0]
    maxX = max(places, key=lambda place: place[0])[0]
    minY = min(places, key=lambda place: place[1])[1]
    maxY = max(places, key=lambda place: place[1])[1]

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.

Drawing

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:

    background(255)
    black = color(0)
    for place in places:
        x, y = place
        set(x, y, black)

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:

    for place in places:
        x, y = place
        try:
            set(x, y, black)
        except Exception, e:
            print "Error drawing place at ({}, {}): {}".format(x, y, e)

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 x and y columns. Think about it before continuing on.

map()

The x and 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:

    for place in places:
        x, y = place
        x = map(x, minX, maxX, 0, width)
        y = map(y, minY, maxY, 0, height)

The variables width and height are builtin to Processing and correspondent to the dimensions of our window (what we gave to size() in our setup() function).

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.

Graphics coordinates

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?

Places

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:

    class Place(objcet):
        minX, maxX = (0, 0)
        minY, maxY = (0, 0)
    
        longitude = 0
        latitude = 0
        name = ""
        postalCode = 0
        population = -1
        density = -1

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[0])
        place.longitude = float(columns[1])
        place.latitude = float(columns[2])
        place.name = columns[4]
        place.population = int(columns[5])
        place.density = float(columns[6])
        
        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 draw() function:

    for place in places:
        place.draw()

Try running the program. If it doesn’t run, try fixing any errors we might have introduced until it works.

Properties

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):

    @property
    def x(self):
        return map(self.longitude, self.minX, self.maxX, 0, width)
    
    @property
    def y(self):
        return map(self.longitude, self.minY, self.maxY, height, 0)

Now, in our draw(self) method, we can remove the two map() calls and use self.x and self.y in the set() function call:

    def draw(self):
        black = color(0)
        set(self.x, self.y, black)

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:

  • size()
  • point(), line(), triangle(), quad(), rect(), ellipse(), and bezier()
  • background(), fill(), stroke(), noFill(), noStroke(), strokeWeight(), strokeCap(), strokeJoin(), smooth(), noSmooth(), ellipseMode(), and rectMode().

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:

Text rendering

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:

    def setup():
        # ...
        labelFont = loadFont("font filename from above.vlw")
        textFont(labelFont, 32) # From now on, all drawing will use the labelFont at 32 point.

Then modify your drawing method to draw some text using text("Hello, France!", x, y) where x and y are the coordinates at which to draw the text.

User events

When the user performs certain actions, such as moving the mouse, pressing or releasing the mouse button, etc, Processing will call certain methods:

  • mousePressed()
  • mouseDragged()
  • mouseReleased()
  • mouseMoved()

You can also access the mouse position with mouseX and mouseY, and you can test which button is pressed with:

    if (mousePressed == true):
        # ...
        
    if (mouseButton == left):
        # ...

Turn-in

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.


Creative Commons License 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.