September 30 (Arrays)

Displaying a Chart

Here’s the weather for the coming week, here in Pittsburgh. Let’s create a simple visualization of the daily high and low temperatures.

weather

sketch

var hi = [72, 65, 66, 57, 58, 65, 64, 67];
var lo = [60, 49, 49, 47, 54, 55, 49, 47];

function setup() {
  createCanvas(480, 100);
}

function draw() {
  background(236);

  var nValues = hi.length;
  var xSpacing = 60;
  var xMargin = 30;

  // Draw the HI data
  noFill();
  stroke(160, 0, 0);
  strokeWeight(2);
  beginShape();
  for (var i = 0; i < nValues; i++) {
    var px = i * xSpacing + xMargin;
    var py = height - hi[i];
    vertex(px, py);
    ellipse(px, py, 3, 3);
  }
  endShape();


  // Draw the LO data
  noFill();
  stroke(0, 0, 160);
  strokeWeight(2);
  beginShape();
  for (var i = 0; i < nValues; i++) {
    var px = i * xSpacing + xMargin;
    var py = height - lo[i];
    vertex(px, py);
    ellipse(px, py, 3, 3);
  }
  endShape();
}

If something is worth doing, it’s worth doing well, and if you’re visualizing data, it’s always a good idea to add axes, labels, and a scale, so that the values can be read easily. Here’s the same data as above, but plotted to enhance the legibility of the data (for example, by stretching the vertical display range). Note how we use the handy map() function in order to stretch the data to the range we want, and how the entire graphic is able to scale dynamically with the canvas.

sketch

var hi = [72, 65, 66, 57, 58, 65, 64, 67];
var lo = [60, 49, 49, 47, 54, 55, 49, 47];
var dayAbbrev  = ["T", "W", "Th", "F", "S", "Su", "M", "T"];

function setup() {
  createCanvas(640, 300);
}

function draw() {
  background(240);

  var nValues = hi.length;
  var xSpacing = width / (nValues + 1);
  var xMargin = xSpacing;

  var minTemp = 40;
  var maxTemp = 80;
  var lowestTempPixel = height - 40;
  var highestTempPixel = 50;

  //-------------
  // Draw the HI data
  noFill();
  stroke(160, 0, 0);
  strokeWeight(2);
  beginShape();
  for (var i = 0; i < nValues; i++) {
    var px = i * xSpacing + xMargin;
    var py = map(hi[i], minTemp, maxTemp, lowestTempPixel, highestTempPixel);
    vertex(px, py);
    ellipse(px, py, 3, 3);
  }
  endShape();

  //-------------
  // Draw the LO data
  noFill();
  stroke(0, 0, 160);
  strokeWeight(2);
  beginShape();
  for (var i = 0; i < nValues; i++) {
    var px = i * xSpacing + xMargin;
    var py = map(lo[i], minTemp, maxTemp, lowestTempPixel, highestTempPixel);
    vertex(px, py);
    ellipse(px, py, 3, 3);
  }
  endShape();

  //-------------
  // Draw the value labels
  fill(0,0,0, 160);
  noStroke();
  textAlign(CENTER);
  for (var i = 0; i < nValues; i++) {
    var px = i * xSpacing + xMargin;
    var pyLo = map(lo[i], minTemp, maxTemp, lowestTempPixel, highestTempPixel);
    var pyHi = map(hi[i], minTemp, maxTemp, lowestTempPixel, highestTempPixel);
    text(lo[i], px, pyLo + 16);
    text(hi[i], px, pyHi - 6);
    text(dayAbbrev[i], px,lowestTempPixel+16);  
  }

  //-------------
  // Draw the axes, with some computed temperature ticks. 
  stroke(0);
  strokeWeight(1);
  line(xMargin / 2, lowestTempPixel, xMargin / 2, highestTempPixel);
  line(xMargin / 2, lowestTempPixel, width - xMargin / 2, lowestTempPixel);

  textAlign(RIGHT);
  for (var t = minTemp; t <= maxTemp; t += 5) {
    var ty = map(t, minTemp, maxTemp, lowestTempPixel, highestTempPixel);
    fill(0,0,0, 160);
    noStroke();
    text(t, xMargin / 2 - 7, ty+4);
    stroke(0);
    line(xMargin / 2 - 4, ty, xMargin / 2, ty);
    stroke(0,0,0, 30);
    line(xMargin/2, ty, width - xMargin / 2, ty);
  }

  //-------------
  // Draw the heading. 
  fill(0); 
  noStroke(); 
  textAlign(LEFT);
  text("Temperatures in Pittsburgh for the week of 9/29/2015 - 10/06/2015 (F°)", xMargin / 2, 30);

}

It’s also possible to automatically scale your data to the visible display area. First one finds the maximum and minimum values of your data; then you use these as the input-range arguments to map():

sketch

var myData = [];
var nData = 6; 

function setup() {
  createCanvas (600, 300); 
  randomizeMyData();
}

//-------------------------------
function draw() {
  background(255,200,200); 

  
  // search for the smallest and largest values in the data
  var smallestValue = 99999;
  var largestValue  =-99999;
  for (var i=0; i<nData; i++){
    if (myData[i] < smallestValue){
      smallestValue =  myData[i];
    }
    if (myData[i] > largestValue){
      largestValue =  myData[i];
    }
  }
  
  var myTop = 75;
  var myBot = height-75; 
  
  fill (0); 
  noStroke();
  text("Data: " + myData.toString(), 5,15); 
  text("Min: " + smallestValue, 5, 30);
  text("Max: " + largestValue, 5, 45);
  fill(255, 96); 
  rect(50, myTop, (nData-1)*100, (myBot-myTop) );
  
  noFill();
  stroke(0); 
  strokeWeight(3); 
  beginShape(); 
  for (var i=0; i<nData; i++){
    var aRawValue = myData[i]; 
    var py = map(aRawValue, smallestValue, largestValue, myBot,myTop); 
    var px = i*100 + 50; 
    vertex(px, py); 
  }
  endShape(); 
  
  
  fill(0); 
  noStroke(); 
  textAlign(CENTER);
  for (var i=0; i<nData; i++){
    var aRawValue = myData[i]; 
    var py = map(aRawValue, smallestValue, largestValue, myBot,myTop); 
    var px = i*100 + 50; 
    text(aRawValue, px, py+20); 
  }

  
}


//-------------------------------
function randomizeMyData(){
  for (var i=0; i<nData; i++){
    myData[i] = round(random(0,100)); 
  }
}

function mousePressed(){
  randomizeMyData();
}

(In the examples to follow, for the sake of having brief code and in order to focus on other issues, we’ll skip the labels. Still, we just wanted you to see what’s involved in drawing a proper chart.)


Searching through an Array

In the next example, we exclusively focus on the daily high temperatures. We wish to find out which day has the coolest (lowest) high temperature. To do this, we have to search through all of the data in the array. This is a really important pattern.

sketch

var hi = [72, 65, 66, 57, 58, 65, 64, 67];

function setup() {
  createCanvas(640, 240);
}

function draw() {
  background(240);

  var nValues = hi.length;
  var xSpacing = width / (nValues + 1);
  var xMargin = xSpacing;
  
  var minTemp = 50;
  var maxTemp = 80;
  var lowestTempPixel = height - 20;
  var highestTempPixel = 20;

  //-------------
  // Draw the HI data
  noFill(); stroke(160, 0, 0); strokeWeight(2);
  beginShape();
  for (var i = 0; i < nValues; i++) {
    var px = i * xSpacing + xMargin;
    var py = map (hi[i], minTemp, maxTemp, lowestTempPixel, highestTempPixel);
    vertex(px, py);
    ellipse(px, py, 3, 3);
  }
  endShape();

  //-------------
  // IMPORTANT PATTERN!
  // Search for the lowest value in the array. 
  // Store the lowest value, and its index in the array. 
  //
  var coolestHiIndex = 0;
  var coolestHiValue = 99999; // start with something ridiculous. 
  for (var i = 0; i < nValues; i++) {
    var ithValue = hi[i];
    if (ithValue < coolestHiValue) {
      coolestHiValue = ithValue;
      coolestHiIndex = i;
    }
  }

  // Let's draw a special indicator around the lowest value. 
  var xCoolest = coolestHiIndex * xSpacing + xMargin;
  var yCoolest = map(coolestHiValue, minTemp, maxTemp, lowestTempPixel, highestTempPixel);
  noFill(); stroke(0, 0, 0); strokeWeight(1);
  ellipse(xCoolest, yCoolest, 20, 20);
  noStroke(); fill(0); textAlign(CENTER);
  text(coolestHiValue, xCoolest, yCoolest + 26);

}

In the next example, we search for the day of the week, which has the least difference between its high and low temperatures. Note how we use functional abstraction in this example.

sketch

var hi = [72, 65, 66, 57, 58, 65, 64, 67];
var lo = [60, 49, 49, 47, 54, 55, 49, 47];

var nValues;
var xSpacing;
var xMargin;

var minTemp = 40;
var maxTemp = 80;
var lowestTempPixel;
var highestTempPixel;

//--------------------------------------
function setup() {
  createCanvas(640, 300);

  nValues = hi.length;
  xSpacing = width / (nValues + 1);
  xMargin = xSpacing;
  lowestTempPixel = height - 20;
  highestTempPixel = 20;
}

//--------------------------------------
function draw() {
  background(240);
  
  drawSomeData (hi, color(160, 0, 0));
  drawSomeData (lo, color(0, 0, 160));
  drawIndicatorOfDayWithLeastDifferenceBetweenHiAndLoTemperatures(); 
}


//--------------------------------------
function drawIndicatorOfDayWithLeastDifferenceBetweenHiAndLoTemperatures(){
  // Fetch the index in the arrays, 
  // at which here is the least difference between the hi and lo values. 
  var leastDifIndex = getIndexWithSmallestDifferenceBetweenLoAndHi();
  var xLeastDif = (leastDifIndex * xSpacing) + xMargin;
  var hiTemp = hi[leastDifIndex];
  var loTemp = lo[leastDifIndex];
  var yLeastDifHi = map(hiTemp, minTemp, maxTemp, lowestTempPixel, highestTempPixel);
  var yLeastDifLo = map(loTemp, minTemp, maxTemp, lowestTempPixel, highestTempPixel);
  
  noFill(); 
  stroke(0);
  strokeWeight(1);
  line(xLeastDif, yLeastDifHi, xLeastDif, yLeastDifLo);
  ellipseMode(CENTER); 
  ellipse(xLeastDif, (yLeastDifHi+yLeastDifLo)/2, 35,70);
}

//--------------------------------------
function getIndexWithSmallestDifferenceBetweenLoAndHi() {
  var leastDifferenceIndex = 0; // note carefully; this one is a global!
  var leastDifference = 99999; // start with something ridiculous.
  for (var i = 0; i < nValues; i++) {
    var ithHiValue = hi[i];
    var ithLoValue = lo[i];
    var difference = abs(ithHiValue - ithLoValue); 
    if (difference < leastDifference) {
      leastDifference = difference;
      leastDifferenceIndex = i;
    }
  }
  return leastDifferenceIndex;
}


//--------------------------------------
function drawSomeData (anArray, aColor) {
  // Draw some data with a color
  noFill();
  stroke(aColor);
  strokeWeight(2);
  beginShape();
  for (var i = 0; i < nValues; i++) {
    var px = i * xSpacing + xMargin;
    var py = map(anArray[i], minTemp, maxTemp, lowestTempPixel, highestTempPixel);
    vertex(px, py);
    ellipse(px, py, 3, 3);
  }
  endShape();
}


Log-Scale Representations

Sometimes one’s data spans several orders of magnitude. Consider the following table of data, representing typical weights (in grams) for different species of animals:

Chick               50
Hamster             60
Rabbit            1000
Chicken           1500
Cat               2000
Small dog         2000
Medium dog        5000
Monkey            5000
Large dogs        8000
Human            90000
Pig             150000
Cow             800000
Giraffe         900000
Horse          1200000
Elephant       5000000
Large whale  120000000

The whale, at 120 million grams, is more than 2 million times more massive than the chick. In order to display such values on the same screen, we take the log() of each value before displaying it. You can see this when you click in the sketch below.

sketch

var masses = [50, 60, 1000, 1500, 2000, 2000, 5000, 5000,
  90000, 150000, 800000, 900000, 1200000, 5000000, 120000000
];
var animals = ["Chick", "Hamster", "Rabbit", "Chicken", "Cat",
  "Small dog", "Medium dog", "Monkey", "Human", "Pig", "Cow",
  "Giraffe", "Horse", "Elephant", "Large whale"
];

var nAnimals;
var xSpacing;
var xMargin;
var textBaseline;
var chartBaseline;

//--------------------------
function setup() {
  createCanvas(640, 400);

  nAnimals = animals.length;
  xSpacing = width / (nAnimals+2);
  xMargin = xSpacing;
  textBaseline = height - 70;
  chartBaseline = height - 85;
}

//--------------------------
function draw() {
  background(255, 200, 200);

  drawAnimalNames();

  if (mouseIsPressed) {
    drawLogAnimalMasses();
  } else {
    drawAnimalMasses();
  }
}

//--------------------------
function drawAnimalMasses() {
  noFill();
  stroke(0);
  strokeWeight(2);
  beginShape();
  for (var i = 0; i < nAnimals; i++) {
    var ax = i * xSpacing + xMargin;
    var ay = chartBaseline - masses[i] / 10000;
    vertex(ax, ay);
    ellipse(ax, ay, 5, 5);
  }
  endShape();
}

//--------------------------
function drawLogAnimalMasses() {
  noFill();
  stroke(0);
  strokeWeight(2);
  beginShape();
  for (var i = 0; i < nAnimals; i++) {
    var ax = i * xSpacing + xMargin;
    var ay = chartBaseline - 10 * log(masses[i]);
    vertex(ax, ay);
    ellipse(ax, ay, 5, 5);
  }
  endShape();
}


//--------------------------
function drawAnimalNames() {
  noStroke();
  fill(0);

  // Draw all of the animal names. 
  for (var i = 0; i < nAnimals; i++) {
    var tx = i * xSpacing + xMargin;
    var ty = textBaseline;
    var anAnimal = animals[i];
    push();
    translate(tx, ty);
    rotate(radians(45));
    text(anAnimal, 0, 0);
    pop();
  }

  stroke(0);
  strokeWeight(1);
  line(xMargin, chartBaseline, width - xMargin, chartBaseline);
}


Shallow, Reference and Deep Copies

There are three kinds of copies: shallow (top level, by value), reference (shared) and deep (complete, by value).

For the time being, if you’re copying an array in JavaScript, you can think of a shallow copy as being a duplicate copy of just the data from the original. Suppose you’ve made a copy; if you modify the copy, the original will not be affected. This is analogous to taking a Xerox copy of a Gutenberg Bible. If you then scribble on the Xerox copy, you’re not committing criminal damage to a priceless object.

A reference copy, on the other hand, acts like a nickname or alias for the original. Suppose you have a nebbish friend named Walter. Now suppose there’s a school bully, and you hear him bragging that he gave “Wally” a painful wedgie. Should you be concerned for for your friend Walter? Yes, yes, you should.

In the example below, a shallow copy and a reference copy are made from an array of numbers. When the user presses ‘A’, it reverses the shallow copy. When the user presses ‘B’, it reverses the reference copy. Observe how the original is affected (or not).

sketch

// Sketch illustrating shallow and reference copies of arrays. 
//
// Data reprsenting heartbeat rates of various animal species.
// http://www.sjsu.edu/faculty/watkins/longevity.htm
var myData = [150, 400, 275, 65, 30, 65, 450, 44, 60, 90, 192, 70, 205, 100, 20];
var myDataCopy1 = [];
var myDataCopy2 = [];

function setup() {
  createCanvas(800, 100);

  // http://devdocs.io/javascript/global_objects/array/slice
  myDataCopy1 = myData.slice();
  myDataCopy2 = myData;
}

//-----------------------------------
function draw() {
  background(255, 200, 200);

  fill(0);
  noStroke();

  text("Original data:", 20, 30);
  text(myData.toString(), 200, 30);


  text("Shallow copy, made by slice():", 20, 50);
  text(myDataCopy1.toString(), 200, 50);
  text("Press 'A' to reverse.", 550, 50);

  text("Reference copy, made with '=':", 20, 70);
  text(myDataCopy2.toString(), 200, 70);
  text("Press 'B' to reverse.", 550, 70);
}

//-----------------------------------
function keyPressed() {
  if (key == 'A') {
    myDataCopy1.reverse();
  }
  if (key == 'B') {
    myDataCopy2.reverse();
  }
}

A deep copy comes into play when an array contains another array or objects. Consider an array of arrays: [[1, 2], [3, 4]]. A shallow copy would copy the elements [1, 2] and [3, 4] by reference, so these would be shared between the original and the copy. To make the original and copy completely independent, you need a deep copy, which will make new independent copies of the arrays [1, 2] and [3, 4].


Push, Pop, Shift, Unshift

“Stacks” and “Queues” represent two different ways of using arrays. Consider the following two examples:

// Stacks: Last In, First Out (LIFO)
var stack = [];
stack.push(2);       // stack is now [2]
stack.push(5);       // stack is now [2, 5]
var i = stack.pop(); // stack is now [2]
print(i);            // displays 5
 
// Queues : First In First Out (FIFO)
var queue = [];
queue.push(2);         // queue is now [2]
queue.push(5);         // queue is now [2, 5]
var i = queue.shift(); // queue is now [5]
print(i);              // displays 2

Here’s a demonstration program.

sketch

// Demonstration of push, pop, shift, unshift. 

var myData = [];

var maxNData;
var margin;
var spacing;
var reportString;

//---------------------------------------
function setup() {
  createCanvas(600, 400);
  margin = 100;
  spacing = 25;
  maxNData = 1 + ((width - 2 * margin) / spacing);
  reportString = "";
}

//---------------------------------------
function draw() {
  background(255, 200, 200);
  drawLegend();


  push();
  translate(100, 150);

  fill(255, 80);
  rect(0, 0, 400, 100);
  noFill();
  stroke(0);
  strokeWeight(2);

  // render the chart.
  beginShape();
  for (var i = 0; i < myData.length; i++) {
    var px = i * spacing;
    var py = 100 - myData[i];
    vertex(px, py);
    ellipse(px, py, 3, 3);
  }
  endShape();
  drawTerminalValues();

  pop();

}

//---------------------------------------
function drawTerminalValues() {
  // write out the two end values. 
  var nVals = myData.length;
  if (nVals > 0) {
    fill(0);
    noStroke();
    textAlign(RIGHT);
    text(myData[0], -5, 100 - myData[0] + 5);
    textAlign(LEFT);
    text(myData[nVals - 1], (nVals - 1) * spacing + 5, 100 - myData[nVals - 1] + 5);
  }
}


//---------------------------------------
function drawLegend() {
  fill(0);
  noStroke();
  text("Keypresses: ", 100, 40)
  text("1: Push (add to end)", 100, 60);
  text("2: Unshift (add to beginning)", 100, 80);
  text("3: Pop (remove from end)", 100, 100);
  text("4: Shift (remove from beginning)", 100, 120);
  text(reportString, 100, 300);
}

//---------------------------------------
function keyPressed() {
  aRandomValue = floor(100 * noise(millis()));
  reportString = "";

  switch (key) {

    case '1':
      myData.push(aRandomValue);
      reportString = "Added " + aRandomValue;
      if (myData.length > maxNData) {
        removedValue = myData.shift();
        reportString += " and removed " + removedValue;
      }
      break;

    case '2':
      myData.unshift(aRandomValue);
      reportString = "Added " + aRandomValue;
      if (myData.length > maxNData) {
        removedValue = myData.pop();
        reportString += " and removed " + removedValue;
      }
      break;

    case '3':
      removedValue = myData.pop();
      reportString += "Removed " + removedValue;
      break;
    case '4':
      removedValue = myData.shift();
      reportString += "Removed " + removedValue;
      break;
  }


}

Other functions we should look at: