Data Viz Transition
For my Nature of Code midterm I decided to focus on making a “transition” for switching between options on a data visualization. The visualization itself is very minimal for now – just squares, each representing a month of my life (each line represents a year).
I used the ToxicLibs Verlet library and had more difficulty with it than expected. The only other physics library I’ve used is Traer Physics, so I kept treating the Verlet library the same way. Eventually I found a good balance between two influences. The first is a very strong negative attraction force between all of the blocks and a centralized (-ish) location on the screen, which is created when the user clicks between the visualization options (started by the mousePressed) and is removed when the mouse is released. The other influence is a spring that holds the moving particle (with rectangle) to an invisible, hidden particle that is anchored at its original starting point, which slowly draws it back in.
I don’t love the jolty spring movement nor the hectic attractor movement, and probably will try to redo this in Box2D at some point.
Code is below, and will try to get it working on here too!
import toxi.geom.*;
import toxi.physics2d.*;
import toxi.physics2d.behaviors.*;
VerletPhysics2D physics;
AttractionBehavior mouseAttractor;
Vec2D mousePos;
int age;
CSV file;
Month []months;
String event;
Month selected;
void setup() {
size(800, 600);
physics = new VerletPhysics2D();
// Set the world's bounding box
physics.setWorldBounds(new Rect(0, 0, width, height));
age=24;
file = new CSV("lifev2.csv", 1);
months=new Month[file.data.length];
for (int i=0;i<file.data.length;i++) {
months[i]=new Month(physics, file.getRow(i));
}
smooth();
noStroke();
}
void draw() {
background(255);
physics.update();
if (event=="lifeEvent") {
fill(100);
}
else {
fill(200);
}
rect(width-115, height/4, 10, 10);
if (event=="relationships") {
fill(100);
}
else {
fill(200);
}
rect(width-115, height/4+20, 10, 10);
if (event=="travel") {
fill(100);
}
else {
fill(200);
}
rect(width-115, height/4+40, 10, 10);
//rect(width-115, height/2+5, 8, 8);
fill(0);
text("Life Events", width-100, height/4+8);
text("Relationships", width-100, height/4+28);
text("Travel", width-100, height/4+49);
//text("neutral", width-100, height/2+12);
for (int i=0;i<months.length;i++) {
months[i].display();
}
if (selected != null){
selected.hovering();
}
}
void mousePressed() {
if (mouseX>=width-115 && mouseX<width-105
&& mouseY>=height/4 && mouseY<=height/4+10) {
if (event != "lifeEvent") {
transition();
}
event="lifeEvent";
}
else if (mouseX>=width-115 && mouseX<width-105
&& mouseY>=height/4+20 && mouseY<=height/4+30) {
if (event != "relationships") {
transition();
}
event="relationships";
}
else if (mouseX>=width-115 && mouseX<width-105
&& mouseY>=height/4+40 && mouseY<=height/4+50) {
if (event != "travel") {
transition();
}
event="travel";
}
}
void transition() {
mousePos = new Vec2D(width/4, height/2);
// create a new positive attraction force field around the mouse position (radius=250px)
mouseAttractor = new AttractionBehavior(mousePos, height, -20f);
physics.addBehavior(mouseAttractor);
}
void mouseReleased() {
// remove the mouse attraction when button has been released
physics.removeBehavior(mouseAttractor);
}
//strip quotes from beginning and end of substring
//getRow and getColumn are not 0 based, they are 1 based
//will break if there are new lines, search for \n in csv file
class CSV {
String [][] data;
int numHeaders;
String [][] headers;
CSV(String fileName, int headerRows) {
String lines[] = loadStrings(fileName);
String chars[];
int dataWidth=0;
numHeaders =headerRows;
//see how long the longest row is
for (int i=0;i<lines.length;i++) {
chars = HJsplit(lines[i], ',');
if (chars.length>dataWidth) {
//dataWidth equals the number of columns!!
dataWidth=chars.length;
}
}
data=new String[lines.length-numHeaders][dataWidth];
for (int i=numHeaders;i<lines.length;i++) {
chars = HJsplit(lines[i], ',');
for (int j=0;j<dataWidth;j++) {
data[i-numHeaders][j]=chars[j];
}
}
headers=new String[numHeaders][dataWidth];
}
String[] HJsplit(String tempString, char tempDelimiter) {
boolean quoteYes = false;
int lastSegment=0;
String [] segments = new String[tempString.length()];
String tempSub;
int segmentNum=0;
//go through each character and see if it's a quote
for (int i=0;i<tempString.length();i++) {
if (tempString.charAt(i)=='\"') {
quoteYes = !quoteYes;
}
if (tempString.charAt(i)==(tempDelimiter)) {
if (!quoteYes) {
tempSub=tempString.substring(lastSegment, i);
segments[segmentNum]=tempSub;
lastSegment=i+1;
segmentNum++;
}
}
}
String [] finalSeg = new String [segmentNum];
for (int i=0;i<segmentNum;i++) {
finalSeg[i]=segments[i];
}
return finalSeg;
}
String [] getRow(int row){
return data[row];
}
String[] getColumn(int column){
String[] tempColumn = new String[data.length];
for (int i = 0; i < data.length; i++){
tempColumn[i]= data[i][column-1];
}
return tempColumn;
}
void dump(){
for(int i=0; i < data.length; i++){
for(int j=0; j<data[0].length; j++){
print(data[i][j]+", ");
}
println();
}
}
}
class Month {
String monthName;
int monthNumber;
int year;
String placeLived;
int age;
String inRelationship;
String lifeEvent;
String travelled;
String travelledUsa;
String goodOrBad;
PVector position;
boolean settled;
VerletParticle2D particle;
VerletParticle2D anchorParticle;
VerletSpring2D spring;
AttractionBehavior selfAttractor;
Month(VerletPhysics2D phys, String []tempLine) {
monthName=tempLine[0];
monthNumber= int(tempLine[1]);
year=int(tempLine[2]);
placeLived=tempLine[3];
age=int(tempLine[4]);
inRelationship=tempLine[5];
travelled=tempLine[6];
travelledUsa=tempLine[7];
lifeEvent=tempLine[9];
goodOrBad=tempLine[10];
anchorParticle = new VerletParticle2D((monthNumber)*21, ((year)-1988)*21+20);
particle = new VerletParticle2D((monthNumber)*21, ((year)-1988)*21+20);
phys.addParticle(particle);
phys.addParticle(anchorParticle);
anchorParticle.lock();
spring=new VerletSpring2D(anchorParticle, particle, 0, 0.001);
physics.addSpring(spring);
// selfAttractor = new AttractionBehavior(mousePos, 250, -1f);
// physics.addBehavior(mouseAttractor);
//particle.lock();
}
void display() {
if (event != null) {
if (event.equals("lifeEvent")) {
fill(0,0,150);
text("Negative Event",width-115,height/2+60);
fill(0,150,0);
text("Positive Event",width-115,height/2+80);
if (lifeEvent.length()>0) {
if (goodOrBad.equals("good")) {
fill(0, 150, 0);
}
else {
fill(0, 0, 150);
}
}
else {
fill(150);
}
}
else if (event.equals("travel")) {
fill(0,0,150);
text("Domestic",width-115,height/2+60);
fill(0,150,0);
text("International",width-115,height/2+80);
if (travelled.length()>0) {
fill(0, 150, 0);
}
else if (placeLived.equals("Peru") ||
placeLived.equals("Ghana") ||
placeLived.equals("France") ||
placeLived.equals("Europe")) {
fill(0, 150, 0);
}
else if (travelledUsa.length()>0) {
fill(0, 0, 150);
}
else {
fill(150);
}
}
else if (event.equals("relationships")) {
if (inRelationship.equals("yes")) {
fill(0, 150, 0);
}
else if (inRelationship.equals("complicated")) {
fill(100, 150, 100);
}
else {
fill(150);
}
}
rect(particle.x, particle.y, 20, 20);
}
fill(0);
if (mouseX>=particle.x && mouseX<=particle.x+20 && mouseY>=particle.y
&& mouseY<=particle.y+20){
selected=this;
}
}
void hovering(){
if (event.equals("travel")){
if (travelled.length()==0){
if (travelledUsa.length()==0){
text(placeLived,mouseX,mouseY);
}
text(travelledUsa,mouseX,mouseY);
}
text(travelled,mouseX,mouseY);
}
if (event.equals("lifeEvent")){
text(lifeEvent,mouseX,mouseY);
}
}
}
