Arrays and objects
In this tutorial, I’m going to show you how to store information about user actions in your sketch. In order to accomplish this, we’re going to talk about two new programming concepts: arrays and objects.
The goal of this tutorial is to create a sketch with the following behavior: whenever the user clicks on the sketch, a rectangle is created. Each rectangle then moves downward until it disappears from the bottom of the screen.
Rectangles everywhere
I already showed you how to make persistent rectangles. You can just tell Processing not to overwrite the background after each frame:
function setup() { createCanvas(400, 400); background(50); rectMode(CENTER); noStroke(); fill(255); } function draw() { // nothing to do! drawing happens in mousePressed } function mousePressed() { rect(mouseX, mouseY, 50, 25); }
We also know how to make it so a single rectangle moves across the screen. With a bit of ingenuity, we can even make this rectangle start moving at the coordinates where the user clicked:
let hasClicked = false; let xpos = 0; let ypos = 0; function setup() { createCanvas(400, 400); } function draw() { background(50); noStroke(); rectMode(CENTER); fill(255); if (hasClicked) { rect(xpos, ypos, 50, 25); ypos += 1; } } function mousePressed() { hasClicked = true; xpos = mouseX; ypos = mouseY; }
(It’s worth looking at the logic of this sketch to make sure you understand how it works. I’ll wait!)
This is all well and good, you say, but we’re still pretty far from our goal: making multiple moving rectangles appear on the screen in response to user input. Of course, it’s easy to conceptualize how we might do this if we had a set number of rectangles beforehand:
let rectY_0 = 0; let rectY_1 = 15; let rectY_2 = 30; function setup() { createCanvas(400, 400); } function draw() { background(50); noStroke(); rectMode(CENTER); fill(255); rect(100, rectY_0, 50, 25); rect(200, rectY_1, 50, 25); rect(300, rectY_2, 50, 25); rectY_0 += 1; rectY_1 += 1; rectY_2 += 1; }
This doesn’t meet our normal rule for programming, which is “avoid repeating yourself,” but it works.
An array of coordinates
Let’s think for a second about how you would “factor out” the repetition in the above example, and how we could make it easier to add more rectangles to the sketch. (This is another small step along the way to our eventual goal of user-controlled rectangles.)
In the past, we’ve used a for
loop to factor out repetition, and on first
glance it seems like we could use a for
loop for this task. Something like:
// note: this isn't actual javascript code!
for (let i = 0; i < 3; i++) {
rect(100, rectY_<i>, 50, 25);
rectY_<i> += 1
}
Javascript has a kind of data structure that is designed to facilitate this very thing, and it’s called an array.
An array is a kind of value that stores a list of other values. You can access the values in an array by their position in the list. You can also add items to the array, or remove items, or modify items that are already in there. If a single value is like a box for storing something (like a number), then an array is like a shelf to put boxes on. Programming metaphors.
Here’s the example above, redone to use an array:
let rectY = [0, 15, 30]; function setup() { createCanvas(400, 400); } function draw() { background(50); noStroke(); rectMode(CENTER); fill(255); for (let i = 0; i < rectY.length; i++) { rect((i+1)*100, rectY[i], 50, 25); rectY[i] += 1; } }
There’s a lot of new stuff in here, so let’s pick it apart one piece at a time. First, there’s the declaration of the array:
let rectY = [0, 15, 30];
When you’re making an array value in Javascript, you can type the values you
want to put into the array right into your program (this is called an array
literal; we’ll talk later about other ways to get arrays into your program).
The statement above creates an array and stores it in a variable called
rectY
.
All arrays have a .length
attribute that tells you how many items there are
in the array. In the first line of the for
loop, we use this to set the
upper-bound of the loop:
for (let i = 0; i < rectY.length; i++) {
Inside the loop, this expression:
rectY[i]
… evaluates to the value stored at that index of the array. (By index I mean the number indicating which “slot” of the array to get an item from.) So, inside the loop, this statement:
rect((i+1)*100, rectY[i], 50, 25);
… draws a rectangle, using the value stored at the particular index of the
list (0
, 1
, or 2
). This value can be modified as well, which is what
happens here:
rectY[i] += 1;
Arrays in more detail
The idioms discussed in the above demonstrate most of the use cases for arrays in Processing. But it’s worth talking about some of the syntax in more detail. For this, we’re going to write statements and expressions in an empty p5.js sketch.
Again, to create an array, use square brackets with comma-separated values inside:
let stuff = [5, 10, 15, 20];
An array can have as many values as you want it to have. It can even be empty to start out with:
let nothingHereMoveAlong = [];
An array has a number of elements. To see how many elements there are in a
given array, use its .length
attribute:
console.log(stuff.length); // displays 4
console.log(nothingHereMoveAlong.length); // displays 0
To get a particular item from an array, put square brackets directly after the variable that holds the array value. Inside the square brackets, supply a number:
console.log(stuff[2]); // displays 15 ...why not 10?! see below
If you supply a number that is beyond the bounds of the array (i.e., larger
than the length of the array), the expression evaluates to a special Javascript
value, undefined
:
console.log(stuff[152]); // displays "undefined"
The console.log()
function can take an array as a parameter, in which case it
simply displays the contents of the array. This is helpful for debugging
purposes:
console.log(stuff); // diplays [5,10,15,20]
You can overwrite the value at a particular index in an array by putting the
name of the variable containing the array, along with a square bracket index,
on the left-hand side of the assignment operator (=
):
stuff[2] = 999;
console.log(stuff); // now displays [5,10,999,20]
The zero index
So wait, why did stuff[2]
evaluate to 15
? That’s the third item in the
array, not the second. “Am I seeing things?” you ask yourself. “Am I losing my
already tenuous grip on reality.” No, your mind is perfectly healthy, at least
when it comes to this particular topic. Indexing of arrays works in a way that
is, at first, a bit counterintuitive: in Javascript, as in most programming
languages, the indexing of arrays is zero-based.
Zero-based means that what to outward appearances is the first element of
an array is referred to as index 0
by Javascript. This means that to get the
first element of an array called stuff
, you need to write the expression
stuff[0]
. To get the last element of an array with four elements, you would
write stuff[3]
(not stuff[4]
).
The reasons for this particular weirdness are unintuitive, and mostly have to do with the history of computer programming. Wikipedia has a good history of zero indexing that is worth a read.
I like to think of array indexes as counting not the item number but instead how far away the item is from the beginning of the list. If it takes one step to get from one item to the next, the first item in the list has a distance of zero steps from the beginning of the list; the second item has a distance of one step, and so forth.
Adding items to an array
Array values have a method called .push()
which adds an item to the list. In
the following example, we use this method to finally end up with something
approximating our original task: adding a falling rectangle to the sketch
whenever the user clicks:
let rectY = []; // start with empty list function setup() { createCanvas(400, 400); } function draw() { background(50); noStroke(); rectMode(CENTER); fill(255); for (let i = 0; i < rectY.length; i++) { rect(200, rectY[i], 50, 25); rectY[i] += 1; } } function mousePressed() { rectY.push(0); }
Pretty cool!
Keeping track of multiple properties
So! We’ve mostly accomplished our goal: clicking the sketch adds a rectangle that moves down the screen. But it’s not exactly what we had in mind initially, right? Let’s expand our goal to include the following: when you click the screen, the rectangle appears at the coordinates of the mouse click and then moves downward from there.
For this, we’ll need to keep track of two values for each rectangle. This is a bit tricky, and there’s more than one way to do it, each with their own benefits and drawbacks. We’re going to implement each of these in turn.
More than one array
Maybe the easiest solution to the problem is simply to have one array to store
each rectangle’s X coordinates, and one array to store each rectangle’s Y
coordinates, so that the X of the first rectangle will be at index 0 of the
rectX
array, and the Y of the first rectangle will be at index 0 of the
rectY
array, etc. Here’s what that looks like:
let rectX = []; // start with empty list let rectY = []; // start with empty list function setup() { createCanvas(400, 400); } function draw() { background(50); noStroke(); rectMode(CENTER); fill(255); for (let i = 0; i < rectY.length; i++) { rect(rectX[i], rectY[i], 50, 25); rectY[i] += 1; } } function mousePressed() { rectX.push(mouseX); rectY.push(mouseY); }
This is good! But there are some drawbacks to this, if you start thinking ahead:
- To add some other attribute to the data we’re storing for each rectangle, we’ll have to make a third array (and then a fourth array, and then a fifth…)
- To remove a “rectangle” from our data, we have to remove that item from all of the arrays. (More on removing items from arrays below.)
- The relationship between
rectX
andrectY
isn’t reflected in the syntax of the program. Another programmer looking at our code might not immediately intuit that these two variables are always meant to be used together.
Arrays inside arrays
Another solution relies on the fact that arrays themselves are values, and so we can store an array inside another array. To demonstrate, type the following into an empty sketch and example the debug output:
let stuff = [];
stuff.push([24, 25]);
stuff.push([26, 27]);
console.log(stuff); // displays [[24,25],[26,27]]
After you run this code, the variable stuff
is an array which has two
elements, both of which are themselves arrays. If you get the value at index
zero, you get an array:
console.log(stuff[0]); // displays [24,25]
To get one of the two values from that array, you need to take the expression that evaluates to the outer array and use the square bracket syntax on that expression to get the inner value out, like so:
console.log(stuff[0][1]); // displays 25
We can use this idiom to create a version of our falling rectangle sketch that has just one array to store information about all rectangles. Each element of that array is itself an array.
let rectXY = []; // start with empty list function setup() { createCanvas(400, 400); } function draw() { background(50); noStroke(); rectMode(CENTER); fill(255); for (let i = 0; i < rectXY.length; i++) { rect(rectXY[i][0], rectXY[i][1], 50, 25); rectXY[i][1] += 1; } } function mousePressed() { rectXY.push([mouseX, mouseY]); }
The beauty of this is that we can easily add a third attribute to each
rectangle simply by adding a third element to the array that we add to the
rectXY
array on each click. In the following example, a random value is
stored in index 2 on each click, and then this number is used to give each
rectangle a random color in draw()
:
let rectXY = []; // start with empty list function setup() { createCanvas(400, 400); } function draw() { background(50); noStroke(); rectMode(CENTER); fill(255); for (let i = 0; i < rectXY.length; i++) { fill(rectXY[i][2]); rect(rectXY[i][0], rectXY[i][1], 50, 25); rectXY[i][1] += 1; } } function mousePressed() { rectXY.push([mouseX, mouseY, random(255)]); }
EXERCISE: Try adding a fourth element to the array that controls how fast the rectangle falls.
Objects
The disadvantage of the arrays-within-arrays scheme is that it’s easy to forget what each element of the inner array means. The numbers aren’t themselves very mnemonic (e.g., why is the X coordinate stored in index 0? What does ‘X’ have to do with “0”?). It would be cool if there were a way in Javascript to store data in a data structure like an array, i.e., it can store multiple values, but with an easier, more mnemonic way to refer to each of the values it contains.
It turns out that just such a data structure exists! It’s called an object. An object can store multiple values, just like an array, but individual values in an object are accessed by a key, not by a numerical index. Objects are just like any other value in Javascript: you can store them in variables, push them into arrays, even store objects as values inside of other objects.
The basic syntax for creating an object value looks like this:
let asteroid = {radius: 100, mass: 460, population: 17};
This statement creates an object value and assigns it to a variable called
asteroid
. This object has three “keys”: radius
, mass
, and population
.
(You can pick the names of the keys; I’m just making a fanciful
space-exploration-themed object here.) To access a value stored for a
particular key, use one of the following syntaxes:
console.log(asteroid["radius"]);
// or:
console.log(asteroid.radius)
You can add a new key/value pair to an object after it has already been created by using the syntax above on the left-hand side of the assignment operator, and the value you want to store on the right:
asteroid.albedo = 7;
// or:
asteroid["albedo"] = 7;
The console.log()
command will display all of the key/value pairs in an
object if you pass an object as a parameter:
// displays {"radius":100,"mass":460,"population":17,"albedo":7}
console.log(asteroid);
It’s easy to make an array of objects and this is one of the main ways you’ll use objects in the course of your career as a computer programmer. Most of the time, you’ll load arrays of objects from some external source (like an API), or you’ll construct an array of objects over the course of the program (using, e.g., data from user input). But you can also write out an actual array of objects using array and object literals like so:
let kitties = [
{age: 14, weight: 12.2},
{age: 3, weight: 8.9},
{age: 8, weight: 11.0}
];
Now kitties
is an array of objects. To get an individual object from this
array, you would write an expression like this:
console.log(kitties[0]); // displays {"age":14,"weight":12.2}
And to get the value for an individual key inside of an object in the list:
console.log(kitties[0]["age"]);
// or
console.log(kitties[0].age);
Here’s an example of looping over a list of objects. This loop prints the sum of the weights of all kitties in the list:
let weightSum = 0;
for (let i = 0; i < kitties.length; i++) {
weightSum += kitties[i].weight;
}
// displays 32.1
console.log(weightSum);
Rectangles as objects
Here’s a version of our rectangle sketch that puts it all together. On each
click, this sketch creates a new object and pushes it into the rectObjs
array. In draw()
, each rectangle is displayed by accessing the values using
the appropriate keys:
let rectObjs = []; // start with empty list function setup() { createCanvas(400, 400); } function draw() { background(50); noStroke(); rectMode(CENTER); fill(255); for (let i = 0; i < rectObjs.length; i++) { fill(rectObjs[i].fillColor); rect(rectObjs[i].xpos, rectObjs[i].ypos, 50, 25); rectObjs[i].ypos += 1; } } function mousePressed() { rectObjs.push({xpos: mouseX, ypos: mouseY, fillColor: random(255)}); }
EXERCISE: Modify the above example so that each rectangle object also stores a separate speed for the X and Y coordinates, then modify the
draw()
loop so that the rectangles move in directions other than downwards. BONUS: Write code so that each rectangle “bounces” when it reaches the bounds of the sketch.
Removing items from an array
Notes TK
let rectObjs = []; // start with empty list function setup() { createCanvas(400, 400); } function draw() { background(50); noStroke(); rectMode(CENTER); fill(255); for (let i = 0; i < rectObjs.length; i++) { fill(rectObjs[i].fillColor); rect(rectObjs[i].xpos, rectObjs[i].ypos, 50, 25); rectObjs[i].ypos += 1; } for (let i = rectObjs.length - 1; i >= 0; i--) { if (rectObjs[i].ypos > height) { rectObjs.splice(i, 1); } } } function mousePressed() { rectObjs.push({xpos: mouseX, ypos: mouseY, fillColor: random(255)}); console.log(rectObjs); // look at debug area! }
Using other people’s data (CSV format)
Not all of the data in the world comes about as the result of physical actions that user take in the course of running a single sketch. You can use p5.js to access data from other data sources. In this section, I’m going to show you how to work with data in CSV format.
Other places to find data:
We’re going to work with this CSV of the season results for the 2020-2021 Utah Jazz, the storied NBA franchise. The data comes from Basketball Reference.
“CSV” stands for “comma separated values.” It’s a plain text file that contains structured data. You can think of the file as essentially a spreadsheet, with the names of the columns in the first line of the file. Each “row” of data is on a subsequent line, with individual cells separated by commas. The CSV format is understood by many different programming languages and tools (you can export CSV from Excel or Google Sheets, for example), and many organizations use CSV as a “data interchange” format for releasing data publicly.
Conceptually, a CSV file is kind of like an array of rows, and each individual row is itself an object whose keys are the column names and values are the cells in that column.
The p5.js library provides a cluster of functions for working with CSV files.
The loadTable()
function loads a CSV file into memory as a special kind of
table
value (this is specific to p5.js). The loadTable()
function should be
called in preload()
; the syntax looks like this:
tableVar = loadTable('your_file.csv', 'csv', 'header');
The tableVar
variable can be named whatever you want. The 'csv'
and
'header'
parameters tell loadTable()
that this is a CSV file specifically,
and that it has header values (which will be used to give names to the columns
in each row).
This table value has several methods that we can call to work with the data in the CSV:
.getRowCount()
: evaluates to the number of rows in the file.getNum(i, "foo")
: evaluates to the value in the cell in rowi
with column name"foo"
(There are other functions you can call to get out data of other types; see the reference for more information.)
Here’s an example sketch that uses the Utah Jazz CSV file to display the scores of each game the Jazz played in the 2014-2015 season:
let season; function preload() { season = loadTable( 'UTA_2021.csv', 'csv', 'header'); } function setup() { createCanvas(400, 400); noLoop(); } function draw() { background(50); for (let i = 1; i < season.getRowCount(); i++) { fill(255); ellipse(i*5, 100 + season.getNum(i, "Tm"), 5, 5); fill(0); ellipse(i*5, 100 + season.getNum(i, "Opp"), 5, 5); } }