/**

Tag relationship visualization in preparation for an experimental navigation system. Tag similarity is visualized as spatial distance and color similarity, and tag frequency is related to font size.

Select a beginning and end tag (which become highlighted red and green, respectively) to see the shortest path between them. Deselect by clicking again, or reselect the end by clicking a new tag.

First, tags are loaded for 18 of my projects, comprised of 104 tags. Each new tag is added as a vertex to a graph, making 30 unique tags, and two tags occuring in the same project are added as a (directional) edge, producing 592 edges. If a vertex or edge is already present, its count is increased. An edge from a to b has length (1 - (b.count / a.count)) — i.e., the negated probability that a implies b. If a always implies b, the edge length is 0.

Edge lengths are used to initialize a force directed graph built on Traer's physics and animation libraries.

Distance from each tag to the three most significant and polarized tags determines RGB color. These three tags are found by taking the mode of multiple K-medoids runs (like K-means, but each centroid is a data point) for K = 3 using Dijkstra's algorithm on the graph as the distance metric.

*/ Graph graph; PFont trebuchet; float lengthMin = 1; float lengthMax = 100; float selectDistance = 15; float rotationSpeed = PI/1000; Vertex cur, begin, end; Vector path = null; void setup() { size(470, 470, P3D); colorMode(RGB, 1); trebuchet = loadFont("TrebuchetMS-48.vlw"); textFont(trebuchet, 10); textAlign(CENTER); graph = loadTags("tags.txt"); cur = begin = end = null; } void draw() { background(1); graph.render(); if(begin != null && end != null) { // render path for(int i = 1; i < path.size(); i++) { Vector3D lastp = ((Vertex) path.get(i-1)).particle.position(); Vector3D curp = ((Vertex) path.get(i)).particle.position(); stroke(0,0,.5,.6); line(lastp.x(), lastp.y(), lastp.z(), curp.x(), curp.y(), curp.z()); ((Vertex) path.get(i)).light = true; } ((Vertex) path.lastElement()).light = false; } cur = graph.at(); noStroke(); if(cur != null) { fill(.5,.5); renderSelection(cur); } if(begin != null) { fill(1,.5,.5,.5); renderSelection(begin); } if(end != null) { fill(.5,1,.5,.5); renderSelection(end); } } void renderSelection(Vertex v) { Vector3D pos = v.particle.position(); pushMatrix(); translate(pos.x(), pos.y(), pos.z()); rotateY(-frameCount*rotationSpeed); ellipse(0,0,selectDistance,selectDistance); popMatrix(); } void mousePressed() { if(cur != null) { if(begin == null && cur != end) begin = cur; else if(begin == cur) begin = null; else if(end == null) { end = cur; } else if(end == cur) end = null; else end = cur; if(begin != null && end != null) { // compile path graph.resetMemos(); // memoization keep us from building a path graph.distance(begin, end); path = new Vector(); Vertex inpath = end; path.add(inpath); while((inpath = inpath.previousVertex) != null) path.add(inpath); } } } void keyPressed() { setup(); } Graph loadTags(String filename) { Graph graph = new Graph(); String[] file = loadStrings(filename); for(int i = 0; i < file.length; i++) { String[] tags = subset(split(file[i], ' '), 1); // all but the first for(int j = 0; j < tags.length; j++) { graph.addVertex(tags[j]); for(int k = 0; k < tags.length; k++) if(j != k) graph.addEdge(tags[j], tags[k]); } } graph.setMedoids(3, 1, 5); return graph; }