Via a tweet from @mhawksey in response to a tweet from @sheilmcn, or something like that, I came across a post by Sheila on the topic of Cloud gazing, maps and networks – some thoughts on #oldsmooc so far. The post mentioned a prototyped mindmap style browser for Cloudworks, created in part to test out the Cloudworks API.
Having tinkered with mindmap style presentations using the d3.js library in the browser before (Viewing OpenLearn Mindmaps Using d3.js; the app itself may well have rotted by now) I thought I’d have a go at exploring something similar for Cloudworks. With a promptly delivered API key by Nick Freear, it only took a few minutes to repurpose an old script to cast a test call to the Cloudworks API into a form that could easily be visualised using the d3.js library. The approach I took? To grab JSON data from the API, construct a tree using the Python networkx library, and drop a JSON serialisation of the network into a templated d3.js page. (networkx has a couple of JSON export functions that will create tree based and graph/network based JSON data structures that d3.js can feed from.
Here’s the Python fragment:
#http://cloudworks.ac.uk/api/clouds/{cloud_id}.{format}?api_key={api_key} import urllib2,json, networkx as nx from networkx.readwrite import json_graph id=cloudscapeID #need logic urlstub="http://cloudworks.ac.uk/api/" urlcloudscapestub=urlstub+"cloudscapes/"+str(id) urlsuffix=".json?api_key="+str(key) ctyp="/clouds" url=urlcloudscapestub+ctyp+urlsuffix entities=json.load(urllib2.urlopen(url)) #print entities #I seem to remember issues with non-ascii before, though maybe that was for XML? Hmmm... def ascii(s): return "".join(i for i in s.encode('utf-8') if ord(i)<128) def graphRoot(DG,title,root=1): DG.add_node(root,name=ascii(title)) return DG,root def gNodeAdd(DG,root,node,name): node=node+1 DG.add_node(node,name=ascii(name)) DG.add_edge(root,node) return DG,node DG=nx.DiGraph() DG,root=graphRoot(DG,id) currnode=root #This simple example just grabs a list of clouds associated with a cloudscape for c in entities['items']: DG,currnode=gNodeAdd(DG,root,currnode,c['title']) #We're going to use the tree based JSON data format to feed the d3.js mindmap view jdata = json_graph.tree_data(DG,root=1) #print json.dumps(jdata) #The page template is defined elsewhere. #It loads the JSON from a declaration in the Javascript of the form: jsonData=%(jdata)s print page_template % vars()
The rendered view is something along the lines of:
You can find the original code here.
Now I know that: a) this isn’t very interesting to look at; and b) doesn’t even work as a navigation surface, but my intention was purely to demonstrate a recipe from getting data out of the Cloudworks API and into a d3.js mindmap view in the browser, and it does that. A couple of obvious next steps: i) add in additional API calls to grow the tree (easy); ii) linkify some of the nodes (I’m not sure I know who to do that at them moment?)
Sheila’s post ended with a brief reflection: “I’m also now wondering if a network diagram of cloudscape (showing the interconnectedness between clouds, cloudscapes and people) would be helpful ? Both in terms of not only visualising and conceptualising networks but also in starting to make more explicit links between people, activities and networks.”
So here’s another recipe, again using networkx but this time dropping the data into a graph based JSON format and using the d3.js force based layout to render it. What the script does is grab the followers of a particular cloudscape, grab each of their followers, and then graph how the followers of a particular cloudscape follow each other.
Because I had some problems getting the data into the template, I also used a slightly different wiring approach:
import urllib2,json,scraperwiki,networkx as nx from networkx.readwrite import json_graph id=cloudscapeID #need logic typ='cloudscape' urlstub="http://cloudworks.ac.uk/api/" urlcloudscapestub=urlstub+"cloudscapes/"+str(id) urlsuffix=".json?api_key="+str(key) ctyp="/followers" url=urlcloudscapestub+ctyp+urlsuffix entities=json.load(urllib2.urlopen(url)) def ascii(s): return "".join(i for i in s.encode('utf-8') if ord(i)<128) def getUserFollowers(id): urlstub="http://cloudworks.ac.uk/api/" urluserstub=urlstub+"users/"+str(id) urlsuffix=".json?api_key="+str(key) ctyp="/followers" url=urluserstub+ctyp+urlsuffix results=json.load(urllib2.urlopen(url)) #print results f=[] for r in results['items']: f.append(r['user_id']) return f DG=nx.DiGraph() followerIDs=[] #Seed graph with nodes corresponding of followers of a cloudscape for c in entities['items']: curruid=c['user_id'] DG.add_node(curruid,name=ascii(c['name']).strip()) followerIDs.append(curruid) #construct graph of how followers of a cloudscape follow each other for c in entities['items']: curruid=c['user_id'] followers=getUserFollowers(curruid) for followerid in followers: if followerid in followerIDs: DG.add_edge(curruid,followerid) scraperwiki.utils.httpresponseheader("Content-Type", "text/json") #Print out the json representation of the network/graph as JSON jdata = json_graph.node_link_data(DG) print json_graph.dumps(jdata)
In this case, I generate a JSON representation of the network that is then loaded into a separate HTML page that deploys the d3.js force directed layout visualisation, in this case how the followers of a particular cloudscape follow each other.
This hits the Cloudworks API once for the cloudscape, then once for each follower of the cloudscape, in order to construct the graph and then pass the JSON version to the HTML page.
Again, I’m posting it as a minimum viable recipe that could be developed as a way of building out Sheila’s idea (though the graph definition would probably need to be a little more elaborate, eg in terms of node labeling). Some work on the graph rendering probably wouldn’t go amiss either, eg in respect of node sizing, colouring and labeling.
Still, what do you expect in just a couple of hours?!;-)