Translate my 2D visualization of awake times to 3D space by incorporating another layer of data. I selected the Dewey classes and itemtypes for this third dimension, none of which I had used for previous visualizations. This required me to run queries on the SPL2 database on a subset marked by individual dewey classes and itemtypes, and subsequently defining the “awake time” in each of these classes. As in my previous work, the awake time is defined as the difference (in minutes) between the first check out of a day and a last checkout of a day.
Make an interactive visualization that lets the user adjust the position and perspective of the visualization using ControlP5 modules, but also has an option to highlight specific layers/dimensions of the data. For the former, I used standard ControlP5 slider objects (upper left corner of the screen) as used widely in class demos. For the selection/highlighting of specific data dimensions, I used a ControlP5 listbox item (upper right corner of the screen). I extended the ControlP5 listbox item class so it can show the currently selected item in a different color than the rest of the list (“active color”).
- panning can be done via three sliders in the x, y and z directions (lower left corner of the screen). This greatly simplifies panning through the three dimensional data matrix in interactive mode, especially when looking at the matrix from wide range of angles or zoom levels covering coarse and more granular scales in one and the same application.
- perspective and distances between layers can also be altered in all three spacial dimensions (lower right corner of the screen). This feature was implemented by modifying the p parameter in p*(a,b,c)*d, where a, b and c are the three dimensional lengths of the individual building blocks and d is the number of the layer.
Other applications of this code include multi-year data sets and the animated visualization of data collected at a high temporal sampling rate.
Code: Select all
// importing peasycam into processing, peasycam is used for manipualting the camera positions
import peasy.test.*;
import peasy.org.apache.commons.math.*;
import peasy.*;
import peasy.org.apache.commons.math.geometry.*;
// setting up PeasyCam
PeasyCam cam;
// including ControlP5
import controlP5.*;
ControlP5 controlP5;
ControlP5 controlP5List;
ListBox l;
PMatrix3D currCameraMatrix;
PGraphics3D g3;
// setting up fonts for text
PFont myHelvetica10 = createFont("Helvetica",10, true);
PFont myHelvetica12 = createFont("Helvetica",12, true);
PFont myHelvetica16 = createFont("Helvetica",16, true);
PFont myHelvetica20 = createFont("Helvetica",20, true);
PFont myHelvetica24 = createFont("Helvetica",24, true);
color colForeground = 0xffaa0000;
color colBackground = 0xff660000;
color colActive = 0xffff0000;
//to store the data table
float [][][] dataMatrix = null;
int numWeekdays = 7;
int numWeeks = 52;
int numDewey = 10;
String[] weekdayNames = {
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
};
String[] deweyNames = {
"1-100",
"100-200",
"200-300",
"300-400",
"400-500",
"500-600",
"600-700",
"700-800",
"800-900",
"900-1000"
};
float factDepth;
float morningShift;
int [][][] dataAwake = null;
int [][][] dataEarliestCout = null;
int [][][] dataLatestCout = null;
int maxAwake; //store max value
int numRowsGlobal = 3300;
int numRows;
int numCols = 8; //# of rows and columns of original data file
PFont myFont1 = createFont( "Helvetica", 24, true);
PFont myFont2 = createFont( "Helvetica", 16, true);
// text formatting variables
float constantMarginOnRows = 160;
float constantMarginOnColumns = 100;
Table myTable;
float blockWidth = 40;
float blockHeight = 60; // initial 80
float blockDepth = 35;
int p5Margin = 30;
int p5Distance = 22;
int buttonWidth = 150;
int buttonHeight = 20;
boolean textFlag= false;
boolean numberFlag = false;
boolean lineFlag = false;
//int minimumNumber = 1;
// init values for interactivity PARAMS:
int transparency = 64;
int alpha;
float pHeight = 0;
float pWidth = 0;
float pDepth = 1;
float xPos = -5;
float yPos = 10;
float zPos = -36;
int deweySelect = 1;
void drawGUI()
{
currCameraMatrix = new PMatrix3D(g3.camera);
camera();
controlP5.draw();
g3.camera = currCameraMatrix;
}
void setupGUI()
{
controlP5= new ControlP5(this);
controlP5.setColorForeground(colForeground);
controlP5.setColorBackground(colBackground);
controlP5.setColorLabel(0xffdddddd);
controlP5.setColorValue(color(220));//(0xffff88ff);
controlP5.setColorActive(colActive);
controlP5.setFont(myHelvetica12);
controlP5.setColorLabel(color(255,128));
//controlP5.setBackground(color(40));
//controlP5.setColorBackground(s2DbuttonBackCol);
//controlP5.setColorForeground(s2DbuttonForeCol);
controlP5.setColorCaptionLabel(color(220));
Tab theParent;
Toggle t1 = controlP5.addToggle("Labels",false, p5Margin, p5Margin + 2 * p5Distance , buttonWidth , buttonHeight) ;
t1.setLabel("Labels");
controlP5.Label lt1 = t1.captionLabel();
lt1.style().marginTop = -23; //move upwards (relative to button size)
lt1.style().marginLeft = 2; //move to the right
//controlP5.addToggle("Text Flag", p5Margin, p5Margin + 2 * p5Distance , buttonWidth , buttonHeight) ;
// controlP5.addToggle("Number Flag",p5Margin, p5Margin + 3 * p5Distance , buttonWidth , buttonHeight) ;
Toggle t = controlP5.addToggle("Awake Times",false, p5Margin, p5Margin + 3 * p5Distance , buttonWidth , buttonHeight) ;
t.setLabel("Awake Times");
controlP5.Label lt = t.captionLabel();
lt.style().marginTop = -23; //move upwards (relative to button size)
lt.style().marginLeft = 2; //move to the right
//controlP5.addToggle("Line Flag",p5Margin, p5Margin + 4 * p5Distance , buttonWidth , buttonHeight) ;
Slider sAlpha = controlP5.addSlider("alpha", (int)255*0.2 ,(int)255*0.8 ,transparency ,p5Margin, p5Margin + 4 * p5Distance , buttonWidth , buttonHeight) ;
controlP5.Label sAlphaLabel = sAlpha.captionLabel();
sAlphaLabel.style().marginLeft = -45; //move horizontally
//block distance and layer perspective sliders
// add blank button for heading
controlP5.addButton("Layer distance & perspective")
.setPosition(p5Margin+1200-30, p5Margin + 680)
.setSize(0 , buttonHeight)
.setColorBackground(color(255))
.setColorForeground(color(255))
.setColorCaptionLabel(color(1));
Slider sPerspX = controlP5.addSlider("pHeight" ,-25 ,25,p5Margin+1200, p5Margin + 680+ p5Distance , buttonWidth , buttonHeight) ;
Slider sPerspY = controlP5.addSlider("pWidth" ,-25 ,25 ,p5Margin+1200, p5Margin + 680+ 2*p5Distance , buttonWidth , buttonHeight) ;
Slider sPerspZ = controlP5.addSlider("pDepth" ,1 ,25 ,p5Margin+1200, p5Margin + 680+ 3*p5Distance , buttonWidth , buttonHeight) ;
controlP5.Label sPerspLabelX = sPerspX.captionLabel();
controlP5.Label sPerspLabelY = sPerspY.captionLabel();
controlP5.Label sPerspLabelZ = sPerspZ.captionLabel();
sPerspLabelX.style().marginLeft = -61; //move horizontally
sPerspLabelY.style().marginLeft = -61; //move horizontally
sPerspLabelZ.style().marginLeft = -61; //move horizontally
//three dimensional panning sliders
// add blank button for heading
controlP5.addButton("3D Panning")
.setPosition(p5Margin+30, p5Margin + 680)
.setSize(0 , buttonHeight)
.setColorBackground(color(255))
.setColorForeground(color(255))
.setColorCaptionLabel(color(1));
Slider sPanning = controlP5.addSlider("xPos" ,-100 ,100,p5Margin, p5Margin + 680+ p5Distance , buttonWidth , buttonHeight) ;
//sPanning.setLabel("xPos");
controlP5.Label sPanningLabel = sPanning.captionLabel();
//sPanningLabel.style().marginTop = 1; //move upwards (relative to button size)
sPanningLabel.style().marginLeft = -42; //move horizontally
Slider sPanningY = controlP5.addSlider("yPos" ,-50 ,50 ,p5Margin, p5Margin + 680+ 2*p5Distance , buttonWidth , buttonHeight) ;
Slider sPanningZ = controlP5.addSlider("zPos" ,-150 ,50 ,p5Margin, p5Margin + 680+ 3*p5Distance , buttonWidth , buttonHeight) ;
//controlP5.addButton("Number Flag",1.0 , p5Margin, p5Margin + 3 * p5Distance , buttonWidth , buttonHeight) ;
controlP5.Label sPanningLabelY = sPanningY.captionLabel();
controlP5.Label sPanningLabelZ = sPanningZ.captionLabel();
sPanningLabelY.style().marginLeft = -42; //move horizontally
sPanningLabelZ.style().marginLeft = -42; //move horizontally
//controlP5List.setColorLabel(color(1,128));
//controlP5List.setColorCaptionLabel(color(1));
//ListBox l;
l = controlP5.addListBox("myList")
.setPosition(p5Margin+1200, p5Margin + 3 * p5Distance)
.setSize(buttonWidth,buttonWidth)
.setItemHeight(14)
.setBarHeight(buttonHeight)
.setColorActive(colActive)
.setColorForeground(colForeground)
.setColorBackground(colBackground)
;
l.captionLabel().toUpperCase(true);
l.captionLabel().set("Select Dewey Class");
//l.captionLabel().setColor(0xffff0000);
l.captionLabel().style().marginTop = 3;
l.valueLabel().style().marginTop = 3;
for (int i=0;i<numDewey;i++) {
ListBoxItem lbi = l.addItem(deweyNames[i], i);
//lbi.setColorBackground(0xffff0000);
//l.setValue(i);
if (i==deweySelect){lbi.setColorBackground(colActive);}
}
controlP5.setAutoDraw(false);
}
void controlEvent(ControlEvent theEvent)
{
if(theEvent.isController())
{
if(theEvent.controller().name()=="Labels") {
if(theEvent.controller().value()==1)
{
textFlag = true;
}
else
{
textFlag = false;
}
}
if(theEvent.controller().name()=="Awake Times") {
if(theEvent.controller().value()==1)
{
numberFlag = true;
}
else
{
numberFlag = false;
}
}
if(theEvent.controller().name()=="alpha") {
transparency = int ( theEvent.controller().value() ) ;
}
if(theEvent.controller().name()=="pHeight") {
pHeight = int ( -theEvent.controller().value() ) ;
}
if(theEvent.controller().name()=="pWidth") {
pWidth = int ( theEvent.controller().value() ) ;
}
if(theEvent.controller().name()=="pDepth") {
pDepth = int ( theEvent.controller().value() ) ;
}
if(theEvent.controller().name()=="xPos") {
xPos = int ( theEvent.controller().value() ) ;
}
if(theEvent.controller().name()=="yPos") {
yPos = int ( theEvent.controller().value() ) ;
}
if(theEvent.controller().name()=="zPos") {
zPos = int ( theEvent.controller().value() ) ;
}
//if(theEvent.controller().name()=="myList") {
// deweySelect = int ( theEvent.controller().value() ) ;
// }
/*
if(theEvent.controller().name()=="View 1") {
cam.lookAt(0,0,0,2800,3000);
}
*/
print("control event from : "+theEvent.controller().name());
println(", value : "+theEvent.controller().value());
}
if (theEvent.isGroup()) {
// an event from a group e.g. scrollList
println(theEvent.group().value()+" from "+theEvent.group());
}
// if(theEvent.isGroup() && theEvent.name().equals("myList")){
// deweySelect = int ( theEvent.value() );
// println("deweyClass: "+deweySelect);
// }
//code block to highlight selected item in dewey scroll list
if(theEvent.name().equals("myList")){
int currentIndex = (int)theEvent.group().value();
println("oldDewey select: "+deweySelect);
if(deweySelect >= 0){//if something was previously selected
l.getItem(deweySelect).setColorBackground(colBackground);
//ListBoxItem previousItem = l.getItem(deweySelect);//get the item
//println(previousItem.getColorBackground());
//previousItem.setColorBackground(colBackground);//and restore the original bg colours
}
deweySelect = currentIndex;//update the selected index
println("new Dewey select: "+deweySelect);
l.getItem(deweySelect).setColorBackground(colActive);//and set the bg colour to be the active/'selected one'...until a new selection is made and resets this, like above
}
}
void draw()
{
// refresh the canvas everyframe
background(255);
//modifying the cell size according to window size
translate( -width/2, -height/2); // Peasycam intially sets lookAt() at (0,0,0) hence we need to transalte the axis
if (controlP5.window(this).isMouseOver())
{
cam.setActive(false);
} else
{
cam.setActive(true);
}
// create base lines
//fill(255, 192);
// creating grid lines in 3D
// if ( lineFlag)
// {
// stroke(64, 128);
// for ( int i =0; i <= blockWidth * (numWeekdays); i+= blockWidth)
// {
// pushMatrix(); //setting up a matrix to keep the current graphi coordinates
// translate(constantMarginOnRows, constantMarginOnColumns); //translate to the correct position
// line( i, 0, 0, i, blockHeight * (numWeeks-1), 0); //creating a line (x,y,z)
// popMatrix(); // going back to previos projection matrix
// }
// for ( int i = 0; i < blockHeight* numWeeks; i+= blockHeight)
// {
// pushMatrix();
// translate(constantMarginOnRows, constantMarginOnColumns);
// line ( 0, i, 0, blockWidth* numWeekdays, i, 0);
// popMatrix();
// }
// }
//
//displaying the cells
//for ( int d = numDewey-1; d>=0; d--) {
for ( int d = 0; d<numDewey; d++) {
if (d == deweySelect) {//case layer is selected
alpha = 255;
} else { //standard setting common for all other layers
alpha = transparency;
}
for ( int i = 0; i< numWeeks; i++) //numWeeks
{
for ( int j = 0; j < numWeekdays; j++) // -1 as the last column is empty, our dataMatrix is smaller than the table by one column
{
if ( dataAwake[i][j][1]!=0)
{
noStroke();
colorMode(HSB);
//draw inner box
pushMatrix(); // doing the same for all the boxes, sadly that is the only way to do this
fill(1, 192, 224* dataAwake[i][j][d]/maxAwake, alpha);//, 224* dataAwake[i][j]/maxAwake );
translate(constantMarginOnRows, constantMarginOnColumns); // translating coordiantes
factDepth = (float)(dataLatestCout[i][j][d]-dataEarliestCout[i][j][d])/24;//hours dependent scaling factor for depth
morningShift = (float)(((dataLatestCout[i][j][d]+dataEarliestCout[i][j][d])/2)-12)/24 * blockHeight;
//int zTranslation = (dataAwake[1][1]*255/ maxAwake);//(dataAwake[i][j]*255/ maxAwake)* 3;
//translate( blockWidth/2 , blockHeight/2 , i*numWeekdays*blockHeight + j*blockHeight + morningShift);
translate( i* blockWidth + blockWidth/2 - xPos*blockWidth + d*pWidth*blockWidth, j* blockHeight + blockHeight/2 + morningShift + yPos* blockHeight + d*pHeight*blockHeight, 1 + zPos*blockDepth - d*pDepth*blockDepth); // translating coordiantes to box position
//data cube only:
//translate( i* blockWidth + blockWidth/2 - xPos*blockWidth , j* blockHeight + blockHeight/2 + morningShift + yPos* blockHeight, 1 + zPos*blockDepth - d*blockDepth); // translating coordiantes to box position
// note the movement in x position, it is half of the box's x length, this is because the box is drawn at the center
//box( blockWidth, blockHeight, (dataMatrix[i][j])* 3 ); // creating a cube of appropriate size
box( blockWidth, blockHeight*factDepth, blockDepth);
// d*pDepth*blockDepth
// ==> d*pDepth*blockDepth
// floating boxes instad of bar graphs
// drawing text over the boxes
if ( numberFlag)
{
// box values
translate( 0, 0, blockDepth/2 + 2 ); // translating coordinates to box position
textFont(myHelvetica16);
textAlign(CENTER, CENTER);
fill(0);
text( dataAwake[i][j][d], 0, 0 );
}
popMatrix();
//draw outer glass box
pushMatrix();
stroke(120);
strokeWeight(1);
//colorMode(RGB);
fill(1, 1, 224, 0);//, 224* dataAwake[i][j]/maxAwake );
translate(constantMarginOnRows, constantMarginOnColumns);
translate( i* blockWidth + blockWidth/2 - xPos*blockWidth + d*pWidth*blockWidth, j* blockHeight + blockHeight/2 + yPos* blockHeight + d*pHeight*blockHeight, 1 + zPos*blockDepth - d*pDepth*blockDepth); // translating coordiantes to box position
//translate( i* blockWidth + blockWidth/2 - xPos*blockWidth , j* blockHeight + blockHeight/2 + yPos* blockHeight, 1 + zPos*blockDepth - d*blockDepth); // translating coordiantes to box position
box( blockWidth, blockHeight, blockDepth);
if (textFlag) {
if (d == 0 || d == numDewey-1) { //label only first and last dimension...
if (i==0) {//case first week of the year ==> write labels on left side of matrix
translate( -1*blockWidth, 0, blockDepth/2 + 2 ); // translating coordinates to box position
textFont(myHelvetica20);
textAlign(RIGHT, CENTER);
fill(66);
text(weekdayNames[j], 0, 0 );
if (j == numWeekdays-1) { //case January
translate( blockWidth, blockHeight, blockDepth/2 + 2 ); // translating coordinates to box position
textFont(myHelvetica20);
textAlign(CENTER, CENTER);
fill(66);
text("January", 0, 0 );
}
} else if (i == 22 & j == 0 & d!=0) { // case title
translate(0, -2*blockHeight, blockDepth/2 + 2 );
textFont(myHelvetica24);
textAlign(CENTER, CENTER);
fill(66);
text( "Awake: Empirical hours of a library - across multiple, adjustable dimensions (darker colors refer to longer awake times)", constantMarginOnRows, constantMarginOnColumns/2 );
} else if ((i ==numWeeks-1) & (j == numWeekdays-1)) { // case December
translate( 0, blockHeight, blockDepth/2 + 2 ); // translating coordinates to box position
textFont(myHelvetica20);
textAlign(CENTER, CENTER);
fill(66);
text("December", 0, 0 );
}
}
}
popMatrix();
}
}
}
}
drawGUI();
} // end of draw
void setup()
{
size(1440,900,OPENGL); // setting up the size of window
if (frame != null) {
frame.setResizable(true); // resizable window
}
//setting up the camera
cam= new PeasyCam( this,0,0,0,1000);
cam.setMinimumDistance(-5000); // how near the camera can get 0,0,0
cam.setMaximumDistance(8000); // how far the camera can travel
//control P5 stuff
controlP5 = new ControlP5(this);
g3 = (PGraphics3D)g;
setupGUI();
background(240); //set background to white
//noLoop(); //to run draw() only once
// initialize the 3D array with number of rows and columns
dataMatrix = new float[numRowsGlobal][numCols][numDewey];
for ( int d = 0; d<numDewey; d++){
myTable = new Table(); //allocating memory to new table
//load the awake data set
String filename = "awake_dewey_" + deweyNames[d] + ".csv";
myTable = loadTable(filename, "header");
//header for col names as follows:
//barcode deweyClass weekday_num week_num awake earliest_cout_hour latest_cout_hour
// assign these variables to contain the number of rows and columns from myTable
numRows = myTable.getRowCount();
numCols = myTable.getColumnCount();
// copy everything from table into a 2D array
maxAwake = 0; //find max awake value [minutes] for scaling of colorbar
for ( int i = 0; i< numRows; i++) // a for loop where i is set to 0 and increments all the way to numRows by 1
{
println(filename);
for ( int j = 0; j< numCols; j++)// a for loop where j is set to 0 and increments all the way to numColumns by 1
{
dataMatrix[i][j][d] = myTable.getInt(i, j); // copying the table integer value at mytable (i,j) position into the dataMatrix
//print( dataMatrix[i][j][d] + " "); // print out the value of dataMatrix
}
if ( dataMatrix[i][4][d] > maxAwake) // this is an if statement which checks for the condition in brackets // if true, it executes the statements in brackets
{
maxAwake = (int)dataMatrix[i][4][d];
}
//println(); // switch to next line in the prompt, improves legibility
}
println ( "maximum value is:"+ maxAwake);
println ( "rowCount: "+ numRows);
println ( "columnCount: "+ numCols);
}
//get sorted week - weekday matrix from getWeekdayWeek function
println("getting sorted week - weekday - dewey matrix...");
dataAwake = getWeekWeekday(dataMatrix, 4); // 4 is colIndex for awake
println("interpolating missing data...");
dataAwake = interpMissing(dataAwake);
dataEarliestCout = getWeekWeekday(dataMatrix, 5); // 4 is colIndex for earliest cout hour
dataLatestCout = getWeekWeekday(dataMatrix, 6); // 4 is colIndex for earliest cout hour
}
// Mapping function to convert SQL output to week-weekday matrix that is easier to plot
// This look up table could be generated much more efficiently, but given the size of the dataset it works OK.
int[][][] getWeekWeekday(float[][][]data, int colIndex) {
int[][][] dataWeekWeekday = new int[numWeeks][numWeekdays][numDewey];
for (int d=1; d<=numDewey; d++) {
for (int w=1; w<=numWeeks; w++) {
for (int wd=1; wd<=numWeekdays; wd++) {
dataWeekWeekday[w-1][wd-1][d-1] = 0;//zero initializer
for (int i=0; i<data.length; i++) {//go through values in data file and sort into week weekday matrix
if (((int)data[i][3][d-1] == w) && ((int)data[i][2][d-1] == wd)) {
if ((int)data[i][colIndex][d-1] > dataWeekWeekday[w-1][wd-1][d-1]) {
dataWeekWeekday[w-1][wd-1][d-1] = (int)data[i][colIndex][d-1];
}
}
}
print( dataWeekWeekday[w-1][wd-1][d-1] + " ");
}
println(); // switch to next line in the prompt, improves legibility
}
}
return dataWeekWeekday;
}
// Interpolation function for missing values in data matrix
int[][][] interpMissing(int[][][]data) {
for (int d=1; d<=numDewey; d++) {
for (int w=1; w<=numWeeks; w++) {
for (int wd=1; wd<=numWeekdays; wd++) {
if (data[w-1][wd-1][d-1] == 0) {
println("zero value detected, interpolate with data for the same day from rest of the year");
int wdaySum = 0;
for (int w1=1; w1<=numWeeks; w1++) {
wdaySum += data[w1-1][wd-1][d-1];
}
data[w-1][wd-1][d-1] = wdaySum/numWeeks;
}
print( data[w-1][wd-1][d-1] + " ");
}
println(); // switch to next line in the prompt, improves legibility
}
}
return data;
}