// // del.icio.us visualisation // // Joshua Schachter's del.icio.us enabled it all, but doesn't let us see the bigger picture. // Matt Jones and Kevan Davis got there first by hand and html, but I think this has some promise. // // http://kevan.org/extispicious // http://blackbeltjones.typepad.com/work/2004/07/my_delicious_ta.html // http://del.icio.us/ // // Currently using a hard-coded version of... // posts.xml - http://del.icio.us/api/posts/recent?count=1000 // tags.xml - http://del.icio.us/api/tags/get? // // This won't be live until I redirect/cache my data with PHP to get around the Java sandbox. // This won't show *your* data unless I use regular Java or if Processing can easily do http-auth. // // Please don't pick this code apart on your own. If you email me, I might have commented it by now! // // tom (at) tom-carden (dot) co (dot) uk // // for the text... BFont font; // translations... float sc = 1.0, tx = 300.0, ty = 300.0; // 'physics', hmm... float const_mass = 30.0, const_friction = 0.9; // picking... boolean gotOne = false, // is something selected already? overOne = false; // is the mouse over something already? class Node { float x, y, fx, fy, vx, vy; float mass; float radius; boolean mouseOver = false, selected = false; Node() { this(10.0, 5.0); } Node(float mass, float radius) { x = y = fx = fy = vx = vy = 0.0; x = random(-500,500); y = random(-500,500); this.mass = mass; this.radius = radius; } void interact() { if (!gotOne && !overOne) { float xdiff = mouseX - screenX(x,y,0); float ydiff = mouseY - screenY(x,y,0); mouseOver = abs(xdiff)/sc < radius && abs(ydiff)/sc < radius; } else { mouseOver = false; } if ((mouseOver || selected) && !overOne) { overOne = true; if(mousePressed) { gotOne = true; selected = true; x = (mouseX-tx)/sc; y = (mouseY-ty)/sc; } else { selected = false; gotOne = false; } } else { fill(0,0,0,20); if (!mousePressed) { selected = false; gotOne = false; } } } void draw() { interact(); if (mouseOver || selected) { fill(255,0,0,20); } else { fill(0,0,0,20); } noStroke(); ellipseMode(CENTER_RADIUS); ellipse(x,y,radius,radius); } void update() { vx += fx / mass; vy += fy / mass; x += vx; y += vy; fx = 0; fy = 0; vx *= const_friction; vy *= const_friction; } } class DeliciousTag extends Node { String name; int count; int linkcount; DeliciousTag(String name, int count) { super(count*const_mass, (10+count)); // 11 + count? this.name = name; this.count = count; } void draw() { super.draw(); fill(0); textFont(font, radius); textMode(ALIGN_CENTER); text(name, x, y); } } class DeliciousPost extends Node { Vector tags; DeliciousPost() { super(); tags = new Vector(); } void draw() { super.draw(); } } class Spring { Node a, b; float length = 50.0; float dist_offset = 0.0; Spring(Node a, Node b) { this.a=a; this.b=b; } void draw() { colorMode(HSB,1.0); stroke(max(min(0.5 + dist_offset/length,1.0),0.0),1,1,0.2); line(a.x,a.y,b.x,b.y); colorMode(RGB,255.0); } void update() { float dist = sqrt(sq(a.x - b.x) + sq(a.y - b.y)); dist_offset = dist - length; float angle = atan2(a.x - b.x, a.y - b.y); a.fx -= sin(angle) * dist_offset; a.fy -= cos(angle) * dist_offset; b.fx += sin(angle) * dist_offset; b.fy += cos(angle) * dist_offset; } } Vector nodes, links; float minx, maxx, miny, maxy; void setup() { hint(NEW_GRAPHICS); size(800,800); //smooth(); links = new Vector(); nodes = new Vector(); hackyParser(); font = loadFont("Verdana.vlw"); } void loop() { background(255); // assess forces for (int i = 0; i < links.size(); i++){ Spring s = (Spring)links.get(i); s.update(); } // collide forces for (int i = 0; i < nodes.size(); i++) { Node a = (Node)nodes.get(i); for (int j = i+1; j < nodes.size(); j++) { Node b = (Node)nodes.get(j); float dist_offset = sqrt(sq(a.x - b.x) + sq(a.y - b.y)) - 3*(a.radius+b.radius); if (dist_offset < 0) { float angle = atan2(a.x - b.x, a.y - b.y); a.fx -= sin(angle) * dist_offset; a.fy -= cos(angle) * dist_offset; b.fx += sin(angle) * dist_offset; b.fy += cos(angle) * dist_offset; } } } // apply forces for (int i = 0; i < nodes.size(); i++) { Node node = (Node)nodes.get(i); node.update(); minx = i == 0 ? node.x : min(minx, node.x); miny = i == 0 ? node.y : min(miny, node.y); maxx = i == 0 ? node.x : max(maxx, node.x); maxy = i == 0 ? node.y : max(maxy, node.y); } float w = (maxx-minx)+100; float h = (maxy-miny)+100; float x = minx-50; float y = miny-50; if (!mousePressed) { tx = (width/2)-(x+(w/2)); ty = (height/2)-(y+(h/2)); sc += (min(width/w,height/h) - sc) / 5.0; } translate( tx, ty, 0 ); scale(sc); overOne = false; for (int i = 0; i < nodes.size(); i++) { Node node = (Node)nodes.get(i); node.draw(); } for (int i = 0; i < links.size(); i++) { Spring c = (Spring)links.get(i); c.draw(); } cursor(overOne ? MOVE : ARROW); } void hackyParser() { Map tagMap = new HashMap(); Vector tags = new Vector(); // parse tags... String lines[] = loadStrings("tags.xml"); for (int i=2; i < lines.length-2; i++) { int namePos = lines[i].lastIndexOf("tag=\"")+5; int endNamePos = lines[i].lastIndexOf('\"'); String name = lines[i].substring(namePos,endNamePos); int tagPos = lines[i].indexOf("count=\"")+7; int endTagPos = lines[i].lastIndexOf("\" tag"); int count = Integer.parseInt(lines[i].substring(tagPos,endTagPos));; DeliciousTag tag = new DeliciousTag(name, count); tags.add(tag); tagMap.put(name, tag); } Vector posts = new Vector(); // parse posts... lines = loadStrings("posts.xml"); for (int i=2; i < lines.length-2; i++) { int tagPos = lines[i].indexOf("tag=\"")+5; int endTagPos = lines[i].indexOf("\" description"); String tagString = lines[i].substring(tagPos,endTagPos); String list[] = split(tagString); DeliciousPost p = new DeliciousPost(); for (int j = 0; j < list.length; j++) { p.tags.add(tagMap.get(list[j])); } posts.add(p); } // create connections for (int i = 0; i < posts.size(); i++) { DeliciousPost p = (DeliciousPost)posts.get(i); for (int j = 0; j < p.tags.size(); j++) { DeliciousTag t = (DeliciousTag)p.tags.get(j); links.add(new Spring(p,t)); } } // populate nodes for (int i = 0; i < posts.size(); i++) { DeliciousPost p = (DeliciousPost)posts.get(i); nodes.add(p); float angle = TWO_PI*(float)i/(float)posts.size(); p.x = 200.0*cos(angle); p.y = 200.0*sin(angle); } for (int i = 0; i < tags.size(); i++) { DeliciousTag t = (DeliciousTag)tags.get(i); nodes.add(t); float angle = TWO_PI*(float)i/(float)posts.size(); } }