#!/usr/bin/ruby # A simple graph generator from twitter (for graphviz) # author: Yoan Blanc # revision: 20070517 # usage: ./graph2.rb # example: ./graph2.rb greut 1 | dot -Tpng -o graph.png require 'rubygems' require 'rio' require 'mofo' require 'image_science' class Person attr_accessor :nick attr_accessor :fullname attr_accessor :friends @@base = "cache/" @@buddies = Hash.new def initialize(nick, fullname) @nick = nick @loaded = false @fullname = fullname @friends = Array.new @dir = "#{@@base}#{@nick}/" @@buddies[nick] = self end def to_s @nick end def buddies @@buddies end def dir @dir end def loaded @loaded end def page "#{@dir}index.html" end def picture "#{@dir}ico.png" end def get_picture (url) unless FileTest.exists? self.picture # not a buddy, myself # load picture tmp = @@base + File.basename(url).slice(/^[^?]*/) rio(url) > rio(tmp) ImageScience.with_image tmp do |img| unless FileTest.exists? @dir Dir.mkdir @dir end img.save self.picture end File.delete tmp end end def url 'http://twitter.com/' + @nick end def load (level) @loaded = true unless FileTest.exists? self.page unless FileTest.exists? @dir Dir.mkdir @dir end rio(self.url) > rio(self.page) end contacts = hCard.find self.page contacts.to_a.each do |buddy| if buddy.url =~ /^http:\/\/twitter.com\/.*/ new_nick = buddy.url.slice /[^\/]*$/ if @@buddies.has_key? new_nick # already in, just find it new = @@buddies[new_nick] @friends << new if level > 1 and not new.loaded new.load level-1 end else # create new person new = Person.new new_nick, buddy.fn # add it to my friends @friends << new # load it if level > 1 new.load level-1 else new.get_picture buddy.photo end end else doc = Hpricot(open(self.page)) self.get_picture((doc/"h2.thumb img")[0]["src"]) end end end def load_buddy (buddy) end end def genGraph( persons ) # Generate a graph of the given size graph = [] size = persons.length size.times do |i| graph[i] = [] size.times do |j| # Does person I knows person J graph[i][j] = persons[i].friends.include? persons[j] end end graph end def genLetter( size ) # Generate a serie of human readable numbers (with letters) # like a, b, c ... aa, ab, ac offset = ?a if size <= 26 ("a"..(offset + size - 1).chr) elsif size <= 26**2 div, rest = size.divmod(26).map {|x| x+offset} ("aa"..(div.chr + rest.chr)) elsif size <= 26**3 div2, rest = size.divmod(26**2) div, rest = rest.divmod(26).map {|x| x+offset} div2 += offset ("aaa"..(div2.chr + div.chr + rest.chr)) else # be a samourai (http://c2.com/cgi/wiki?SamuraiPrinciple) raise "Size of the graph is too big for the letter mapping" end end def buildDot( persons, graph, dive ) # Build the dot semantic for GraphViz size = graph[0].length letters = genLetter( size ) dot = "digraph noname {\n" dot += " center = false\n" dot += " ratio = fill\n" dot += " node [style=filled,fontsize=10,width=0.000,height=0.000]\n" # Nice colors and label (with level in it) # Not required without colors neither label nodes = letters.to_a depth = dive.length size.times do |i| dot += " #{nodes[i]}" depth.times do |n| unless dive[n].index(i).nil? color = 1.0 - n/depth.to_f dot += ' [' if FileTest.exists? persons[i].picture dot += 'shapefile="'+persons[i].picture+'",label=""' else dot += 'label="'+persons[i].nick+' ('+n.to_s+')"' end dot += 'color="'+format("%.3f", color)+' 1.000 1.000"]' break end end dot += "\n" end # The connections between nodes size.times do |i| dot += " #{nodes[i]}->{" size.times do |j| if graph[i][j] dot += " #{nodes[j]}" end end dot += " }\n" end dot + "}" end def diveGraph( graph, node, level) # Dive into the graph and find every nodes that are accessible for the given # node. It'll stop at the level (if targeted) that is the max depth. size = graph.length dive = Array.new level.times do |i| dive << Array.new if i == 0 # first level is self dive[i] << node else # find all nodes that are accessible at this level dive[i-1].each do |node| size.times do |j| dive[i] << j if graph[node][j] end end # Remove already known nodes dive[i].uniq! dive[i] = dive[i] - dive[0..i-1].flatten # no more, leave break if dive[i].length == 0 end end dive end person = Person.new ARGV[0], "root" person.load(ARGV[1].to_i) buddies = person.buddies.values graph = genGraph(buddies) dive = diveGraph(graph, buddies.index(person), 50) puts buildDot(buddies, graph, dive)