Skip to content

Instantly share code, notes, and snippets.

@mohamedkhanafer
Created February 7, 2020 22:58
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mohamedkhanafer/1abb3f8b8b7d2d46b2ca2367bdbd25de to your computer and use it in GitHub Desktop.
Save mohamedkhanafer/1abb3f8b8b7d2d46b2ca2367bdbd25de to your computer and use it in GitHub Desktop.
Final visualization
license: mit
<!DOCTYPE html>
<!-- The purpose of this project is to transform the data presented in a bar chart into something more interactive while at the same time keeping the main message. To be able to come up with this final result, we combined various sources with the main ones being a tutorial from FlowingData showing us of to do such a visualization. The link can be found in the notes in the sidebar of the graph on the left side.-->
<html lang="en">
<meta charset="utf-8">
<!-- We are connecting with D3 library-->
<script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<!-- We chose 2 Google fonts for our animation: Source Sans Pro and Roboto Mono (for the small notes)-->
<link href="https://fonts.googleapis.com/css?family=Source Sans Pro:100,200,300,400,500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Roboto Mono:100,200,300,400,500&display=swap" rel="stylesheet">
<!-- Here we input the two headlines -->
<p id="h1">Time allocation of young moms </p>
<p id="h2"> Animation by Group A</p>
<!-- We start with the styling part -->
<style>
/* The styling of the headers */
#h1 {
font-size: 39px;
margin: 0px 0px 2px 460px;
font-family: 'Source Sans Pro', sans-serif;
font-weight: 300;
font-variant: small-caps slashed-zero;
color: #4094aa;
}
#h2 {
font-size: 18px;
margin: 0px 0px 0px 595px;
font-family: 'Source Sans Pro', sans-serif;
font-weight: 100;
font-variant: small-caps slashed-zero;
color: #5cadc1;
}
/* The styling of the container, including the graph */
#main-wrapper {
font-family:'Roboto Mono',sans-serif;
font-size: 15px;
width: 1200px;
/* The styling of the sidebar, making sure the limits of the lines are well aligned */
}
#sidebar {
width: 270px;
float: left;
margin-right: 30px;
margin-top:50px;
padding-right: 20px;
border-right: 1px solid #eaeaea;
height: 800px;
text-align: center;
}
/* This is the styling of the orange timeline, positionning it centered with the other elements*/
#current_time {
font-size: 25px;
color: #fe9001;
text-align: center;
margin: 180px 20px 20px 0px;
}
/* The styling of the chart, ading also lines to make a good frame */
#chart { width: 820px;
float: left;
margin-right: 30px;
margin-top:42px;
padding-right: 20px;
border-right: 1px solid #eaeaea;
border-top: 1px solid #eaeaea;
padding-top: -5px;
height: 800px;
text-align: center;
}
/* This is the styling of the 3 togglebuttons, choosing what colors they should be when selected and unselected. */
#speed {
font-family:'Roboto Mono',sans-serif;
font-size: 13px;
text-transform: uppercase;
margin: 4px 0 20px 25px;
}
#speed .togglebutton {
padding: 3px 3px;
text-align: center;
border: 1px solid #ccc;
cursor: pointer;
float: left;
width: 60px;
color:#7f7f7f;
}
#speed .togglebutton.slow {
border-right: none;
color:#7f7f7f;
}
#speed .togglebutton.fast {
border-left: none;
color:#7f7f7f;
}
#speed .togglebutton.current {
color: #fff;
background: #4094aa;
border: 1px solid #4094aa;
}
/* This part deals with the scrolling notes below the togglebuttons. We decided to make them gray for the audience not to get attracted by them. Note that the gray colour is set later in the D3 part. */
#note {
position: relative;
color: #fff;
padding-left: 5px;
padding-top: 110px;
font-family: 'Source Sans Pro', sans-serif;
font-style: italic;
font-weight: 400;
top: 50px;
font-size: 17px;
line-height: 1.4em;
}
/* This is the styling of the lower note, where we cite our sources*/
#cite {
font-family:'Roboto Mono',sans-serif;
position: absolute;
border-top: 1px solid #eaeaea;
padding-top: 20px;
top: 750px;
font-size: 12px;
line-height: 1.4em;
width: 259px;
color: #93c9d6;
}
/* This is the styling of the explanation just above the orange timeline.*/
#expl {
position: absolute;
padding-top: 30px;
border-top: 1px solid #eaeaea;
top: 125px ;
font-size: 12px;
font-family: 'Roboto Mono', sans-serif;
line-height: 1.4em;
width: 259px;
color: #93c9d6
;
}
/* This is to style the links we have inserted.*/
.line {
/* fill: none;*/
/* stroke: #000;*/
stroke-width: 0.1px;
}
a {
color: #49a3ba;
text-decoration: none;
border-bottom: 1px solid #efefef;
}
a:hover {
border-bottom: 1px solid #000000;
}
.clr { clear: both; }
</style>
<!-- We here define the various blocks of the body, giving them an id or a class that we can use in the javacript later on.-->
<body>
<div id="main-wrapper">
<div id="sidebar">
<div id="current_time"></div>
<div id="speed">
<div class="togglebutton slow current" data-val="slow">Slow</div>
<div class="togglebutton medium" data-val="medium">Fast</div>
<div class="togglebutton fast" data-val="fast">Pause</div>
<div class="clr"></div>
</div>
<div id="note" style="top: 20px; color: rgb(0, 0, 0);"></div>
<div id="cite">
Using <a href="https://twitter.com/beeonaposy/status/1215830967719485441">Caitlin Hudon's bargraph</a> we decided to complement it with an animation based on <a href="https://flowingdata.com/2016/08/23/make-a-moving-bubbles-chart-to-show-clustering-and-distributions/">Nathan Yau</a>'s tutorial.
Thanks for the great inspiration, Caitlin and Nathan!
</div>
<div id="expl">
How a woman spends her time changes significantly once having a baby. This visualization takes us throughout the first months of the parenting adventure.
</div>
</div>
<div id="chart"><svg width="220" height="50">
<div class="clr"></div>
<!-- We start with the Javascript part-->
<script>
// We identify user speed as slow in the beginning of the script so that the nodes start moving right away as we open the page with the visualization. So slow is the default value of our speed that starts without clicking on it. And this attribute is edited later on.
var USER_SPEED = "slow";
var width = 800,
height = 800,
padding = 1,
maxRadius = 3;
// We are initializing everything to 0, including the timer which is the curr_time in here
var sched_objs = [],
curr_minute = 0;
//Here we are defining what will be the centers of the clusters of each activity. Note that the index assigned here is the one connected to the dataset. So in the dataset when we have "1,240" this means the node will go to Sleeping and stay there for 240 units of time.
var act_codes = [
{"index": "1", "short": "SLEEPING", "desc": "Sleeping"},
{"index": "2", "short": "WORKING OR COMMUTING", "desc": "Work/Commute"},
{"index": "3", "short": "FREE TIME", "desc": "Free Time"},
{"index": "4", "short": "NURSING OR PUMPING", "desc": "Nursing / Pumping"},
{"index": "5", "short": "CARING FOR BABY", "desc": "Taking Care of Baby"}
];
//Here we are defining how fast the toggle buttons should be. Note that we do not have a medium button, this one has become the FAST shown in the graph and the "fast" here has become the PAUSE button shown. We made it very slow for the pause button, in order to make sure the graph would actually stop when pause is pressed (there should be another way to setting the pause button but this is the best alternative we came up with)
var speeds = { "slow": 70, "medium": 30, "fast": 10000 };
//Here we are inserting the content of the side notes apearing in the sidebar. We are as well inserting their timings of when they should appear and disappear. Note that later on in the code, we explain how we fixed the timings and what they mean.
var time_notes = [
{ "start_minute": 1, "stop_minute": 60, "note": "Let the animation begin!" },
{ "start_minute": 90, "stop_minute": 210, "note": "First the routine is balanced between work, sleep, and free time." },
{ "start_minute": 240, "stop_minute": 340, "note": "The baby has arrived!" },
{ "start_minute": 370, "stop_minute": 450, "note": " Routine = Baby." },
{ "start_minute": 480, "stop_minute": 560, "note": "Less nursing, more 'Me-time'." },
{ "start_minute": 590, "stop_minute": 690, "note": "Mom is still on maternity leave." },
{ "start_minute": 720, "stop_minute": 820, "note": "Time to get back to work." },
{ "start_minute": 850, "stop_minute": 940, "note": "Older baby, a bit more free time." },
{ "start_minute": 960, "stop_minute": 1060, "note": "Back to a routine with a baby in the equation." },
{ "start_minute": 1100, "stop_minute": 1170, "note": "And last but not least..." },
{ "start_minute": 1200, "stop_minute": 1300, "note": "...Slowly reaching an equilibrium." },
{ "start_minute": 1370, "stop_minute": 1420, "note": "The end..." },
];
// We set the index of the notes to 0 so that the function later on starts from the 1st note set here.
var notes_index = 0;
// Here we assign the Sleeping activity in the center of the circle arrangement
var center_act = "Sleeping",
center_pt = { "x": 380, "y": 345 };
// Here we used a function by Nathan Yau that sets the coordinate of the rest of the activities.
var foci = {};
act_codes.forEach(function(code, i) {
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 };
}
});
// Here we set the SVG object
var svg = d3.select("#chart").append("svg")
.attr("width", width)
.attr("height", height);
//Here we load the data. We uploaded the data in our gist and then paste the link here to connect the data
d3.csv('https://gist.githubusercontent.com/mohamedkhanafer/c98be5b6981264a6d8054a0ae0436898/raw/e620fb6a09bd9a0e8f501d2de4ba93104622610c/gistfile1.txt', function(data) {
//Here we explain how to better read the data. This part explains that each row in the dataset is a node, and it gives the direction the node should take and the time it should stay in that position. For example "3,240,5,240,4,240,2,240,5,240,1,240" is saying that node 1 should go to position 3 (Free Time) and stay 240 units of time (explained later). Then go to position 5 (Taking care of baby) and stay for the same amount of time. And so on.
data.forEach(function(d) {
var day_array = d.day.split(",");
var activities = [];
for (var i=0; i < day_array.length; i++) {
// This sets the duration (the time component is best explained at the bottom)
if (i % 2 == 1) {
activities.push({'act': day_array[i-1], 'duration': +day_array[i]});
}
}
sched_objs.push(activities);
});
// Here we use act_counts to later on calculate the percentages everytime the nodes move.
var act_counts = {"1": 0, "2": 0, "3": 0, "4": 0, "5": 0};
// Here we assign a node for each amount of time in the day. We split this time in hundred and thus, one node is in effect 1% of the time of the woman.
var nodes = sched_objs.map(function(o,i) {
var act = o[0].act;
act_counts[act] += 1;
var init_x = foci[act].x + Math.random();
var init_y = foci[act].y + Math.random();
return {
act: act,
radius: 6,
x: init_x,
y: init_y,
color: color(act),
moves: 0,
next_move_time: o[0].duration,
sched: o,
}
});
//Here we create the force layout in our graph, this will use the nodes. Here, the values for gravity() and charge() are 0 so that nodes don’t all go to only one point on the screen and also don’t push each in the process. (We found that this force layout concept was very well explained in Mike Bostock's post: https://github.com/d3/d3-3.x-api-reference/blob/master/Force-Layout.md)
var force = d3.layout.force()
.nodes(nodes)
.size([width, height])
.gravity(0)
.charge(0)
.friction(.9)
.on("tick", tick)
.start();
//Here we are drawing c aircle for each node.
var circle = svg.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("r", function(d) { return d.radius; })
.style("fill", function(d) { return d.color; });
// Here we are adding the activity labels (also shown in Yau's tutorial).
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;
}
});
//This part styles the labels from within the D3 script. Here for example we fixed the characteristics of the labels. The first part is to edit the actual text in the labels.
label.append("tspan")
.attr("x", function() { return d3.select(this.parentNode).attr("x"); })
.attr("text-anchor", "middle")
.style("fill", "#1f3c4a")
.text(function(d) {
return d.short;
});
// This part is to fix the percentages part of the labels.
label.append("tspan")
.attr("dy", "1.3em")
.attr("x", function() { return d3.select(this.parentNode).attr("x"); })
.attr("text-anchor", "middle")
.attr("class", "actpct")
.style("fill", "#1f3c4a")
.text(function(d) {
return act_counts[d.index] + "%";
});
// Here the nodes are updated based on activity and the duration set in the dataset.
function timer() {
d3.range(nodes.length).map(function(i) {
var curr_node = nodes[i],
curr_moves = curr_node.moves;
// This tells the node to go to the 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;
}
// This subtracts from it from the current activity count
act_counts[curr_node.act] -= 1;
curr_node.act = curr_node.sched[ curr_moves ].act;
// This adds to the new activity count
act_counts[curr_node.act] += 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;
// This is for updating the percentages everytime the nodes move
label.selectAll("tspan.actpct")
.text(function(d) {
return readablePercent(act_counts[d.index]);
});
// This is for updating the time (the numbers are explained later on)
var true_minute = curr_minute % 1440;
d3.select("#current_time").text(minutesToTime(true_minute));
// Here we updated the notes that appear, using the index of the notes set earlier.
if (true_minute == time_notes[notes_index].start_minute) {
d3.select("#note")
.style("top", "0px")
.transition()
.duration(600)
.style("top", "20px")
.style("color", "#8c8c8c")
.text(time_notes[notes_index].note);
}
// This is for the transition to make the note disappear in the end.
else if (true_minute == time_notes[notes_index].stop_minute) {
d3.select("#note").transition()
.duration(1000)
.style("top", "300px")
.style("color", "#ffffff");
notes_index += 1;
if (notes_index == time_notes.length) {
notes_index = 0;
}
}
setTimeout(timer, speeds[USER_SPEED]);
}
setTimeout(timer, speeds[USER_SPEED]);
//The tick(e) (coming from a Mike Bostock's example) continuously runs on loop, and changes the position as well as the color of the circles as needed.
function tick(e) {
var k = 0.04 * e.alpha;
// This pushes the nodes to their designated focus.
nodes.forEach(function(o, i) {
var curr_act = o.act;
// This sets the slugginesh of the movements of the nodes from the activities.
if (curr_act == "0") {
var damper = 0.5;
} else {
var damper = 0.5;
}
o.color = color(curr_act);
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", function(d) { return d.color; })
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
// This function is for resolving the 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;
});
};
}
// This is to set the speed toggles mechanism. This sets when to switch the colours of the buttons when you select a different speed or when you pause it. Again note here that the "medium" was switched to a fast pace and the "fast" to the pause button.
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");
});
}); // @end d3.tsv
// Here we are giving the colors according to the activity
function color(activity) {
var colorByActivity = {
"0": "#e0d400",
"1": "#1c8af9",
"2": "#51BC05",
"3": "#FF7F00",
"4": "#DB32A4",
"5": "#00CDF8",
}
return colorByActivity[activity];
}
// Here we used the counting of percentages to calculate the ongoing displayed numbers.
function readablePercent(n) {
var pct = 100 * n / 100;
if (pct < 1 && pct > 0) {
pct = "<1%";
} else {
pct = Math.round(pct) + "%";
}
return pct;
}
// Here is where we explain how we fixed the timings from the original model. The original one used minutes as timeline. Our data had to use months. And because we have 6 different periods, we divided the inital 1440 minutes into 240 minutes parts and each represented a period. That is why here we added the coding of what to display in the given timings. Thus "if(run=="4am" || run=="5am" || run=="6am"|| run=="7am")" is just saying that for the first 240 minutes, it is equivalent to a month for our data.
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;
}
run=hh + ampm
if(run=="4am" || run=="5am" || run=="6am"|| run=="7am")
return "Pre-Pregnancy"
else if (run=="8am" || run=="9am" || run=="10am" ||run=="11am")
return "1 Month old Baby";
else if (run=="12pm" || run=="1pm" || run=="2pm" || run=="3pm")
return "2 Months old Baby";
else if (run=="4pm" || run=="5pm" || run=="6pm" || run=="7pm")
return "3 Months old Baby";
else if (run=="8pm" || run=="9pm" || run=="10pm" || run=="11pm")
return "4 Months old Baby";
else if (run=="12am" || run=="1am" || run=="2am" || run=="3am")
return "5-6 Months old Baby"
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment