index.html
<div class="meetup-pane">
</div>
<svg class="timeline">
</svg>
<svg class="force">
</svg>
<button class="back">Prev</button>
<button class="forward">Next</button>
<div id="imgCache"></div>
_.md
[ <a href="http://tributary.io/inlet/5543774">Launch: 1024 members</a> ] 5543774 by milroc<br>[ <a href="http://tributary.io/inlet/5537943">Launch: 1024 members</a> ] 5537943 by enjalot<br>[ <a href="http://tributary.io/inlet/5537659">Launch: 1024 members</a> ] 5537659 by enjalot<br>
config.json
{"description":"1024 members","endpoint":"","display":"div","public":true,"require":[{"name":"crossfilter","url":"http://square.github.io/crossfilter/crossfilter.v1.min.js"}],"fileconfigs":{"inlet.js":{"default":true,"vim":false,"emacs":false,"fontSize":12},"meetups.json":{"default":true,"vim":false,"emacs":false,"fontSize":12},"rsvps.json":{"default":true,"vim":false,"emacs":false,"fontSize":12},"members.json":{"default":true,"vim":false,"emacs":false,"fontSize":12},"_.md":{"default":true,"vim":false,"emacs":false,"fontSize":12},"config.json":{"default":true,"vim":false,"emacs":false,"fontSize":12},"style.css":{"default":true,"vim":false,"emacs":false,"fontSize":12},"index.html":{"default":true,"vim":false,"emacs":false,"fontSize":12}},"fullscreen":false,"play":false,"loop":false,"restart":false,"autoinit":true,"pause":true,"loop_type":"period","bv":false,"nclones":15,"clone_opacity":0.4,"duration":3000,"ease":"linear","dt":0.01}
inlet.js
var baseAvatarUrl = "http://photos1.meetupstatic.com/photos/member"
var avatarWidth = 20;
var avatarHeight = 20;
var membersDict = {};
var members = tributary.members;
members.sort(function(a,b) {
return a.joined - b.joined
})
members.forEach(function(member) {
membersDict[member.id] = member;
});
var imgCache = d3.select('#imgCache').selectAll('img').data(members);
imgCache.enter()
.append('img')
.attr({'src': function(d) { return baseAvatarUrl + d.avatar; }, 'width': avatarWidth, 'height': avatarHeight});
var firstMember = tributary.members[0];
var lastMember = tributary.members[tributary.members.length - 1];
var meetupsDict = {};
var meetups = tributary.meetups;
meetups.sort(function(a,b) {
return a.time - b.time
})
meetups.forEach(function(meetup) {
meetupsDict[meetup.id] = meetup;
});
var memberXf = crossfilter(members);
var memberDims = {
"joined": memberXf.dimension(function(d) { return d.joined })
}
var dayGroup = memberDims.joined.group(function(d) { return d3.time.day.floor(new Date(d)) });
var membersByDay = dayGroup.all();
var xf = crossfilter(tributary.rsvps);
var dims = {
"member": xf.dimension(function(d) { return d.id }),
"events": xf.dimension(function(d) { return d.evt }),
"rsvp": xf.dimension(function(d) { return d.response })
};
dims.rsvp.filter('yes');
var display = d3.select("#display")
var svg = d3.select("svg.timeline")
var width = tributary.sw;
var height = tributary.sh;
var timelineHeight = 100;
var timeScale = d3.time.scale()
.domain([firstMember.joined, lastMember.joined])
.range([0, width])
var mbdScale = d3.scale.linear()
.domain(d3.extent(membersByDay, function(d) { return d.value }))
.range([0,50])
svg.selectAll(".meetup")
.data(tributary.meetups)
.enter()
.append("rect")
.classed("meetup", true)
.attr({
x: function(d,i) { return timeScale(d.time) },
y: 0,
width: 1,
height: timelineHeight
})
svg.selectAll(".members-by-day")
.data(membersByDay)
.enter()
.append("rect")
.classed("members-by-day", true)
.attr({
x: function(d,i) { return timeScale(d.key) },
y: function(d,i) { return timelineHeight - mbdScale(d.value) },
height: function(d,i) { return mbdScale(d.value) },
width: 1
})
var xyGravity = [0, 0.1];
var nodes = []
var radius = d3.scale.sqrt().range([4, 9]);
var padding = 6;
var force = d3.layout.force()
.size([width, height])
.nodes(nodes)
.gravity(0.0968)
.charge(0.444)
.friction(0.41762304)
.on("tick", tick)
.start();
function getMember(f) {
return function(d,i) {
var c = membersDict[d.key];
return f(c,i);
}
}
var canvas = d3.select("canvas.force").attr('width', width).attr('height', height);
function tick() {
var context = fsvg.selectAll("g.node")
.each(gravity(xyGravity))
.each(getMember(collide(0.5)))
.select("circle")
.attr({
cx: getMember(function(d) { return d.x }),
cy: getMember(function(d) { return d.y })
})
}
function showMeetup(meetup) {
var meetupPane = display.select(".meetup-pane")
var members = memberDims.joined.filter([0, meetup.time])
var rsvpers = dims.events.filter(meetup.id);
var nRsvpers = rsvpers.top(Infinity).length;
var nMembers = members.top(Infinity).length;
meetupPane.selectAll("*").remove();
meetupPane.append("span").classed("name", true)
.text(meetup.name);
meetupPane.append("span").classed("rsvps", true)
.text(nRsvpers + " RSVPS")
meetupPane.append("span").classed("nmembers", true)
.text(nMembers + " Members total")
var x = svg.selectAll(".meetup").classed("active-meetup", false)
.filter(function(d) { return d.id === meetup.id })
.classed("active-meetup", true);
var newnodes = dims.member.group().all()
.filter(function(d) { return d.value > 0 });
var gnodes = fsvg.selectAll("g.node")
.data(newnodes, function(d) { return d.key });
var baseRadius = 10;
var startRadius = 200;
var enter = gnodes.enter()
.append("g")
.classed("node", true)
.each(function(c,i) {
var key = c.key;
d = membersDict[key];
d.key = key;
d.x = width/2 + Math.cos(Math.random() * 2 * Math.PI) * startRadius;
d.y = height/2 + Math.sin(Math.random() * 2 * Math.PI) * startRadius;
d.px = d.x + 0;
d.py = d.y + 0;
d.radius = radius(baseRadius);
nodes.push(d);
})
enter
.append("circle")
.attr({
cx: getMember(function(d) { return d.x }),
cy: getMember(function(d) { return d.y })
})
.transition()
.duration(100)
.attr({
r: getMember(function(d) { return d.radius }),
fill: getMember(function(d) { return "url(#pattern" + d.id + ")" })
})
enter.append("defs")
.append("pattern")
.attr({
id: function(c) { return "pattern" + c.key }
, width: avatarWidth
, height: avatarHeight
})
.append("image")
.attr({
"xlink:href": getMember(function(d) { return baseAvatarUrl + d.avatar })
, x: baseRadius
, y: baseRadius
, width: avatarWidth
, height: avatarWidth
});
var exit = gnodes.exit()
.each(function(c) {
var d = membersDict[c.key];
var idx = nodes.indexOf(d)
if(idx >= 0)
nodes.splice(idx, 1);
})
exit
.select("circle")
.transition()
.duration(1000)
.attr({
r: 0
})
exit.transition()
.duration(1000)
.remove();
gnodes.select("circle")
.attr({
r: getMember(function(d) { return d.radius })
})
force.resume();
}
function gravity(g) {
return function(d) {
d.x += g[0]
d.y += g[1]
}
}
function collide(alpha) {
var quadtree = d3.geom.quadtree(nodes);
return function(d) {
var r = d.radius + radius.domain()[1] + padding,
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + quad.point.radius + (d.color !== quad.point.color) * padding;
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2
|| x2 < nx1
|| y1 > ny2
|| y2 < ny1;
});
};
}
var activeMeetupIndex = 0;
var backButton = display.select("button.back")
.on("click", function() {
if(activeMeetupIndex > 0) {
activeMeetupIndex--;
showMeetup(meetups[activeMeetupIndex]);
}
})
var forwardButton = display.select("button.forward")
.on("click", function() {
if(activeMeetupIndex < meetups.length-1) {
activeMeetupIndex++;
showMeetup(meetups[activeMeetupIndex]);
}
})
showMeetup(meetups[activeMeetupIndex]);
meetups.json
[{"id":"52020632","name":"Discuss Transitions","venue":"Epicenter Cafe","time":1329442200000},{"id":"53698992","name":"Open Data Hacking at the Library","venue":"Sf public Library Main Branch","time":1330643700000},{"id":"55266432","name":"Getting your Data Drivers License","venue":"swissnex San Francisco ","time":1331863200000},{"id":"57477392","name":"Getting Started with Nutrition and BART","venue":"Nokia Research Center Palo Alto","time":1333503000000},{"id":"64825422","name":"Visualization Open Mic","venue":"BitTorrent","time":1337824800000},{"id":"84262482","name":"Open Data Hacking at the Library","venue":"Sf public Library Main Branch","time":1349472600000},{"id":"102356032","name":"d3.selectAll('meetup').data([2013]).enter()","venue":"Trulia","time":1361500200000},{"id":"51422112","name":".enter() (the first d3.js user group meeting)","venue":"Epicenter Cafe","time":1328835600000},{"id":"52773222","name":"Real-time Dashboards","venue":"Boundary HQ","time":1330050600000},{"id":"54700892","name":"Co-Working Session","venue":"Noisebridge","time":1331173800000},{"id":"55699192","name":"Put some d3 source files up on the screen and discuss","venue":"San Francisco Public Library; Potrero Branch","time":1332281700000},{"id":"58592772","name":"Exploratory Graph Visualization","venue":"Twitter","time":1334280600000},{"id":"57231572","name":"Underscore.js and Canvas","venue":"Tolman Hall, University of California, Berkeley, Berkeley, CA 94720","time":1335313800000},{"id":"63143652","name":"D3 BOF at Fluent Conference 2012","venue":"Hilton Hotel","time":1338346800000},{"id":"67903012","name":"Visual Perception and Illusions","venue":"Groupon","time":1339723800000},{"id":"74765322","name":"d3 goes mobile","venue":"Twitter","time":1344562200000},{"id":"75754562","name":"Rackspace Visualization Hack Day","venue":"Rackspace San Francisco","time":1346428800000},{"id":"84485982","name":"Data geography with d3.geo","venue":"RocketSpace Inc","time":1350610200000},{"id":"102072932","name":"Urban Data Challenge Kick-off","venue":"swissnex San Francisco ","time":1360204200000},{"id":"103428452","name":"Urban Data Hackathon","venue":"Gray Area Foundation for the Arts (GAFFTA)","time":1361642400000},{"id":"111696362","name":"Urban Data Challenge Awards","venue":"swissnex San Francisco ","time":1365271200000}]
style.css
svg.timeline {
width:100%;
height:120px;
position:absolute;
left:0;
bottom:0;
}
svg.force {
position:absolute;
top:0;
left:0;
width: 100%;
height: 100%;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events:none;
}
.meetup {
fill: #ffffff;
}
.active-meetup {
stroke: #ff0000;
}
.members-by-day {
}
.meetup-pane {
float:left;
width: 400px;
text-align: center
}
.meetup-pane span {
clear:left;
float:left;
}
.node circle {
background-image:
}