Last active
June 1, 2016 12:28
-
-
Save danharr/7e70d642f8bcef557549d0fb0d5b1d5c to your computer and use it in GitHub Desktop.
time series example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var USER_SPEED = "slow"; | |
var sched_objs = []; | |
var act_codes = [ | |
{"index": "0", "short": "Downloads", "desc": "Sleeping"}, | |
{"index": "1", "short": "PS Store", "desc": "Personal Care"}, | |
{"index": "2", "short": "Games", "desc": "Eating and Drinking"}, | |
{"index": "3", "short": "Video App", "desc": "Education"}, | |
{"index": "4", "short": "PS Now", "desc": "Work and Work-Related Activities"}, | |
{"index": "5", "short": "Share Play", "desc": "Household Activities"}, | |
{"index": "6", "short": "Remote Play", "desc": "Care for others / Volunteer Activities"}, | |
{"index": "7", "short": "Party", "desc": "Socializing, Relaxing, Leisure, Sports, Exercise, and Recreation"}, | |
{"index": "8", "short": "Live from PlayStation", "desc": "Religious and Spiritual Activities"}, | |
{"index": "9", "short": "Misc.", "desc": "Telephone Calls, Other"}, | |
{"index": "10", "short": "Offline", "desc": "Traveling"} | |
]; | |
var cat_key = [0,1,2,3,4,5,6,6,2,1,7,7,8,6,9,9,10]; | |
var speeds = { "slow": 1000, "fast": 50 }; | |
var treemap_arr = []; | |
var pause = true; | |
// Load data | |
d3.tsv("https://gist.githubusercontent.com/danharr/980fc7587ea78c4b8c5b57d18fed9df1/raw/cbba9b33e84ec3489c8ef62b18cfd4f8dcf859e8/data.tsv", function(error, data) { | |
data.forEach(function(d) { | |
var day_array = d.day.split(","); | |
var activities = []; | |
var treemap_obj = []; | |
for (var i=0; i < day_array.length; i++) { | |
// Duration | |
if (i % 2 == 1) { | |
var act = day_array[i - 1]; | |
var act_index = activities.findIndex(function (v) { | |
return v.act == act; | |
}); | |
var child_obj; | |
switch(+act) { | |
case 0: | |
case 1: | |
case 2: | |
case 3: | |
case 4: | |
case 5: | |
case 6: | |
if (act_index = -1) { | |
activities.push({'act': act, 'duration': +day_array[i]}); | |
} else { | |
activities[act_index].duration += +day_array[i]; | |
} | |
child_obj = {"name": act, "size": +day_array[i]}; | |
break; | |
case 7: | |
case 13: | |
if (act_index = -1) { | |
activities.push({'act': '6', 'duration': +day_array[i]}); | |
} else { | |
activities['6'] += +day_array[i]; | |
} | |
child_obj = {"name": '6', "size": +day_array[i]}; | |
break; | |
case 8: | |
if (act_index = -1) { | |
activities.push({'act': '2', 'duration': +day_array[i]}); | |
} else { | |
activities['2'] += +day_array[i]; | |
} | |
child_obj = {"name": '2', "size": +day_array[i]}; | |
break; | |
case 9: | |
if (act_index = -1) { | |
activities.push({'act': '1', 'duration': +day_array[i]}); | |
} else { | |
activities['1'] += +day_array[i]; | |
} | |
child_obj = {"name": '1', "size": +day_array[i]}; | |
break; | |
case 10: | |
case 11: | |
if (act_index = -1) { | |
activities.push({'act': '7', 'duration': +day_array[i]}); | |
} else { | |
activities['7'] += +day_array[i]; | |
} | |
child_obj = {"name": '7', "size": +day_array[i]}; | |
break; | |
case 12: | |
if (act_index = -1) { | |
activities.push({'act': '8', 'duration': +day_array[i]}); | |
} else { | |
activities['8'] += +day_array[i]; | |
} | |
child_obj = {"name": '8', "size": +day_array[i]}; | |
break; | |
case 14: | |
case 15: | |
if (act_index = -1) { | |
activities.push({'act': '9', 'duration': +day_array[i]}); | |
} else { | |
activities['9'] += +day_array[i]; | |
} | |
child_obj = {"name": '9', "size": +day_array[i]}; | |
break; | |
case 16: | |
if (act_index = -1) { | |
activities.push({'act': '10', 'duration': +day_array[i]}); | |
} else { | |
activities['10'] += +day_array[i]; | |
} | |
child_obj = {"name": '10', "size": +day_array[i]}; | |
break; | |
} | |
treemap_obj.push(child_obj); | |
} | |
} | |
sched_objs.push(activities); | |
// clone a | |
var a2 = JSON.parse(JSON.stringify(treemap_obj)); | |
// make sure elements with the same id are next to each other | |
a2.sort(function (x, y) { | |
if (x['name'] < y['name']) { | |
return -1; | |
} | |
if (x['name'] > y['name']) { | |
return 1; | |
} | |
return 0; | |
}); | |
// iterate over each one, if this one has the same id as the previous one, accumulate | |
// else add to b | |
var lastId; | |
var b = []; | |
for (var i = 0; i < a2.length; i++) { | |
if (lastId == a2[i]['name']) { | |
b[b.length-1]['size'] += a2[i]['size']; | |
} else { | |
b[b.length] = (a2[i]); | |
lastId = a2[i]['name']; | |
} | |
} | |
}); | |
runVis(); | |
function runVis () { | |
var width = 780, | |
height = 800, | |
padding = 1, | |
maxRadius = 3; | |
var curr_minute = 300; | |
// Activity to put in center of circle arrangement | |
var center_act = "Traveling", | |
center_pt = { "x": 380, "y": 365 }; | |
// Coordinates for activities | |
var foci = {}; | |
var act_index = []; | |
act_codes.forEach(function(code, i) { | |
act_index.push(code.short); | |
if (code.desc == center_act) { | |
foci[code.index] = center_pt; | |
} else { | |
var theta = 2 * Math.PI / (act_codes.length-1); | |
foci[code.index] = {x: 250 * Math.cos(i * theta)+380, y: 250 * Math.sin(i * theta)+365 }; | |
} | |
}); | |
// Start the SVG | |
var svg = d3.select("#chart").append("svg") | |
.attr("width", width) | |
.attr("height", height); | |
var margin = {top: 40, right: 10, bottom: 160, left: 60}; | |
// Used for percentages by minute | |
var act_counts = [{"index": 0, "count": 0}, {"index": 1, "count": 0}, {"index": 2, "count": 0}, | |
{"index": 3, "count": 0}, {"index": 4, "count": 0}, {"index": 5, "count": 0}, | |
{"index": 6, "count": 0}, {"index": 7, "count": 0}, {"index": 8, "count": 0}, | |
{"index": 9, "count": 0}, {"index": 10, "count": 0}]; | |
var color = d3.scale.ordinal() | |
.range(["#bebada","#ffffb3","#8dd3c7","#bebada","#fb8072","#80b1d3","#fdb462","#b3de69","#fccde5", | |
"#d9d9d9","#bc80bd"]); | |
// A node for each person's schedule | |
var nodes = sched_objs.map(function (o, i) { | |
var act = o[0].act; | |
act_counts[+act].count += 1; | |
var init_x = foci[act].x + Math.random(); | |
var init_y = foci[act].y + Math.random(); | |
return { | |
act: act, | |
radius: 3, | |
x: init_x, | |
y: init_y, | |
color: "#00cdc0", //color(act), | |
moves: 0, | |
next_move_time: o[0].duration, | |
sched: o | |
} | |
}); | |
var force = d3.layout.force() | |
.nodes(nodes) | |
.size([width, height]) | |
// .links([]) | |
.gravity(0) | |
.charge(0) | |
.friction(.9) | |
.on("tick", tick) | |
.start(); | |
var circle = svg.selectAll("circle") | |
.data(nodes) | |
.enter().append("circle") | |
.attr("r", function (d) { | |
return d.radius; | |
}) | |
//.style("fill",function(d,i){return color(d.act)}) | |
.style("opacity",0.6) | |
.style("stroke","black") | |
.attr("class","dot") | |
//.on("click", function (d) { | |
//updateTreemap(treemap_arr[d.index]); | |
//}) | |
; | |
console.log("nodes"); | |
console.log(nodes); | |
console.log("act_codes"); | |
console.log(act_codes); | |
// Activity labels | |
var label = svg.selectAll("text") | |
.data(act_codes) | |
.enter().append("text") | |
.attr("class", "actlabel") | |
.attr("x", function (d, i) { | |
if (d.desc == center_act) { | |
return center_pt.x; | |
} else { | |
var theta = 2 * Math.PI / (act_codes.length - 1); | |
return 340 * Math.cos(i * theta) + 380; | |
} | |
}) | |
.attr("y", function (d, i) { | |
if (d.desc == center_act) { | |
return center_pt.y; | |
} else { | |
var theta = 2 * Math.PI / (act_codes.length - 1); | |
return 340 * Math.sin(i * theta) + 365; | |
} | |
}); | |
label.append("tspan") | |
.attr("x", function () { | |
return d3.select(this.parentNode).attr("x"); | |
}) | |
// .attr("dy", "1.3em") | |
.attr("text-anchor", "middle") | |
.text(function (d) { | |
return d.short; | |
}); | |
label.append("tspan") | |
.attr("dy", "1.3em") | |
.attr("x", function () { | |
return d3.select(this.parentNode).attr("x"); | |
}) | |
.attr("text-anchor", "middle") | |
.attr("class", "actpct") | |
.text(function (d) { | |
return readablePercent(act_counts[d.index].count); | |
}); | |
// } | |
/*********************/ | |
/* BAR CHART */ | |
/*********************/ | |
// Update Ranking | |
// Update nodes based on activity and duration | |
function timer() { | |
d3.range(nodes.length).map(function (i) { | |
var curr_node = nodes[i], | |
curr_moves = curr_node.moves; | |
// Time to go to next activity | |
if (curr_node.next_move_time == curr_minute) { | |
if (curr_node.moves == curr_node.sched.length - 1) { | |
curr_moves = 0; | |
} else { | |
curr_moves += 1; | |
} | |
// Subtract from current activity count | |
act_counts[+curr_node.act].count -= 1; | |
// Move on to next activity | |
curr_node.act = curr_node.sched[curr_moves].act; | |
// Add to new activity count | |
act_counts[+curr_node.act].count += 1; | |
curr_node.moves = curr_moves; | |
curr_node.cx = foci[curr_node.act].x; | |
curr_node.cy = foci[curr_node.act].y; | |
nodes[i].next_move_time += nodes[i].sched[curr_node.moves].duration; | |
} | |
}); | |
force.resume(); | |
curr_minute += 1; | |
// Update percentages | |
label.selectAll("tspan.actpct") | |
.text(function (d) { | |
return readablePercent(act_counts[+d.index].count); | |
}); | |
// Update times | |
var true_minute = curr_minute % 1440; | |
d3.select("#current_time").text(minutesToTime(true_minute)); | |
if (!pause) { | |
setTimeout(timer, speeds[USER_SPEED]); | |
} | |
} | |
// Time Control | |
d3.select("#play").style("display", "initial").on("click", function () { | |
setTimeout(timer, speeds[USER_SPEED]); | |
pause = false; | |
d3.select("#play").style("display", "none"); | |
d3.select("#pause").style("display", "initial"); | |
}); | |
d3.select("#pause").on("click", function () { | |
pause = true; | |
d3.select("#play").style("display", "initial"); | |
d3.select("#pause").style("display", "none"); | |
}); | |
d3.select("#reset").style("display", "initial").on("click", reset); | |
function tick(e) { | |
var k = 0.04 * e.alpha; | |
// Push nodes toward their designated focus. | |
nodes.forEach(function (o, i) { | |
var curr_act = o.act; | |
// Make sleep more sluggish moving. | |
if (curr_act == "0") { | |
var damper = 0.6; | |
} else { | |
var damper = 1; | |
} | |
o.color = "#00cdc0"; | |
o.y += (foci[curr_act].y - o.y) * k * damper; | |
o.x += (foci[curr_act].x - o.x) * k * damper; | |
}); | |
circle | |
.each(collide(.5)) | |
// .style("fill", "none") | |
.attr("cx", function (d) { | |
return d.x; | |
}) | |
.attr("cy", function (d) { | |
return d.y; | |
}) | |
.style("fill","blue") | |
//.style("stroke","white") | |
//.style("stroke-width",6) | |
//.style("fill",function(d,i){return color(d.act)}) | |
.attr("class","dot"); | |
} | |
// Resolve collisions between nodes. | |
function collide(alpha) { | |
var quadtree = d3.geom.quadtree(nodes); | |
return function (d) { | |
var r = d.radius + maxRadius + 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.act !== quad.point.act) * 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; | |
}); | |
}; | |
} | |
// Speed toggle | |
d3.selectAll(".togglebutton") | |
.on("click", function () { | |
if (d3.select(this).attr("data-val") == "slow") { | |
d3.select(".slow").classed("current", true); | |
d3.select(".medium").classed("current", false); | |
d3.select(".fast").classed("current", false); | |
} else if (d3.select(this).attr("data-val") == "medium") { | |
d3.select(".slow").classed("current", false); | |
d3.select(".medium").classed("current", true); | |
d3.select(".fast").classed("current", false); | |
} | |
else { | |
d3.select(".slow").classed("current", false); | |
d3.select(".medium").classed("current", false); | |
d3.select(".fast").classed("current", true); | |
} | |
USER_SPEED = d3.select(this).attr("data-val"); | |
}); | |
function reset() { | |
if (! pause) { | |
d3.select("#play").style("display", "initial"); | |
d3.select("#pause").style("display", "none"); | |
pause = true; | |
} | |
d3.select("#current_time").html("9:00am"); | |
d3.selectAll("svg").remove(); | |
curr_minute = -1; | |
runVis(); | |
} | |
} | |
}); // @end d3.tsv | |
// Output readable percent based on count. | |
function readablePercent(n) { | |
var pct = 100 * n / 1000; | |
if (pct < 1 && pct > 0) { | |
pct = "<1%"; | |
} else { | |
pct = Math.round(pct) + "%"; | |
} | |
return pct; | |
} | |
// Minutes to time of day. Data is minutes from 4am. | |
function minutesToTime(m) { | |
var minutes = (m + 4*60) % 1440; | |
var hh = Math.floor(minutes / 60); | |
var ampm; | |
if (hh > 12) { | |
hh = hh - 12; | |
ampm = "pm"; | |
} else if (hh == 12) { | |
ampm = "pm"; | |
} else if (hh == 0) { | |
hh = 12; | |
ampm = "am"; | |
} else { | |
ampm = "am"; | |
} | |
var mm = minutes % 60; | |
if (mm < 10) { | |
mm = "0" + mm; | |
} | |
return hh + ":" + mm + ampm | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment