Multi-foci bubble charts. Visualization originally created for a piece on the diversity of Canada's Heritage Minutes.
Last active
April 19, 2016 20:18
-
-
Save tomcardoso/1aa4e5a2d3912e178bd3 to your computer and use it in GitHub Desktop.
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
[ | |
["title","time","singlePerson","group","race","gender","quote" ], | |
["Lucille Teasdale","1960s","Yes","No","White","Female","" ], | |
["Laura Secord","1810s","Yes","No","White","Female","Take me to Fitzgibbon." ], | |
["Peacemaker","Unknown","Yes","No","Indigenous","Male","Does the great peace still have power? You're here, aren't you?" ], | |
["Vikings","1980s","No","Yes","White","Male","" ], | |
["John Cabot","1490s","Yes","No","White","Male","Fishes enough to feed this kingdom - o, sire, until the end of time!" ], | |
["Jacques Cartier","1530s","Yes","No","White","Male","But I'm sure it means the houses, the village." ], | |
["Jean Nicollet","1630s","Yes","No","White","Male","Mississippi! The sea! China!" ], | |
["Syrup","Unknown","No","Yes","Indigenous","Both","" ], | |
["Governor Frontenac","1690s","Yes","No","White","Male","I will reply from the mouth of my cannon!" ], | |
["Hart & Papineau","1800s","No","Yes","White","Male","" ], | |
["Etienne Parent","1830s","Yes","No","White","Male","Our two races can live side by side without one side enslaving the other." ], | |
["Baldwin & Lafontaine","1840s","No","Yes","White","Male","Mr. Lafontaine, think of the history we'll make when a French-Canadian runs and wins in York." ], | |
["Responsible gov't","1840s","Yes","No","White","Female","Responsible government. ... It's a Canadian idea." ], | |
["Orphans","1850s","No","No","White","Both","" ], | |
["Underground Railroad","1840s","No","No","Black","Both","Liza!" ], | |
["Joseph Casavant","1830s","Yes","No","White","Male","I call it the Marching Thunder!" ], | |
["The Paris Crew","1860s","No","Yes","White","Male","" ], | |
["Saguenay Fire","1870s","No","No","White","Both","His hair's on fire!" ], | |
["Jennie Trout","1870s","Yes","No","White","Female","There's no place for women in medical school!" ], | |
["Sitting Bull","1870s","Yes","No","Indigenous","Male","I do not want lies! These men ... are the first white men who never lied to us!" ], | |
["Les Voltigeurs de Quebec","1880s","No","Yes","White","Male","Gentlemen, this piece deserves better!" ], | |
["Sir Sandford Fleming","1860s","Yes","No","White","Male","We aren't just building a railroad, gentlemen! We are building a country!" ], | |
["Rural Teacher","1880s","No","Yes","White","Female","" ], | |
["Soddie","1890s","No","No","White","Both","" ], | |
["Midwife","Unknown","No","Yes","White","Female","" ], | |
["Basketball","1900s","Yes","No","White","Male","Is this some kind of Canadian joke, sir?" ], | |
["Sam Steele","1890s","Yes","No","White","Male","Why didn't I shoot him?" ], | |
["Frontier College","1920s","No","No","White","Male","" ], | |
["Marconi","1900s","Yes","No","White","Male","Through the air, across the ocean. The first time - ever." ], | |
["Grey Owl","Unknown","Yes","No","White","Male","Men become what they dream. You have dreamed well." ], | |
["Valour Road","1910s","No","No","White","Male","" ], | |
["Winnie","1910s","No","No","White","Male","Oh, Daddy. I just love Winnie. Couldn't we take him home with us?" ], | |
["Nitro","1880s","No","Yes","Asian","Male","You see, there is one dead Chinese man for every mile of the track!" ], | |
["John McCrae","1910s","Yes","No","White","Male","John McCrae died in the war, but his poem is still read when children gather to remember." ], | |
["J.S. Woodworth","1920s","Yes","No","White","Male","Is it too much to ask that Canada take care of poor people, 70 years of age, who helped build this country?" ], | |
["Halifax Explosion","1910s","No","No","White","Male","" ], | |
["Joseph-Armand Bombardier","1920s","Yes","No","White","Male","" ], | |
["Emily Murphy","1920s","Yes","No","White","Female","I, Emily Murphy of Alberta, and all Canadian women after me ... Persons, under the law." ], | |
["Superman","1930s","Yes","No","White","Male","No ones going to read a comic strip about a superhero in tights, Joe. It'll never fly." ], | |
["Myrnam Hospital","1930s","No","No","White","Both","" ], | |
["La Bolduc","1930s","Yes","No","White","Female","" ], | |
["Inukshuk","1930s","No","No","Indigenous","Both","Now the people will know we were here." ], | |
["Wilder Penfield","1930s","Yes","No","White","Male","Dr. Penfield, I can smell burnt toast!" ], | |
["Agnes Macphail","1930s","Yes","No","White","Female","Is this normal?" ], | |
["Bluenose","1930s","No","Yes","White","Male","Just once more, old girl. Then you can rest." ], | |
["Emily Carr","Unknown","Yes","No","White","Female","At last, I knew I must see through the eye of the totem itself ... The mythic eye of the forest." ], | |
["Pauline Vanier","1930s","Yes","No","White","Female","" ], | |
["Marion Orr","1940s","Yes","No","White","Female","You know what my dream is? To go home when this is all over. Find a grass strip somewhere in Ontario ... Teach flying." ], | |
["Maurice \"Rocket\" Richard","1940s","Yes","No","White","Male","" ], | |
["Jackie Robinson","1940s","Yes","No","Black","Male","" ], | |
["John Humphrey","1940s","Yes","No","White","Male","Say, isn't that the Canadian who actually wrote the declaration of human rights?" ], | |
["Avro Arrow","1950s","No","No","White","Both","" ], | |
["Stratford","1950s","No","Yes","White","Both","" ], | |
["Paule Emile Borduas","1940s","Yes","No","White","Male","Life goes on. The important thing is to be able to create - isn't it?" ], | |
["Le Reseau","1950s","Yes","No","White","Male","" ], | |
["Maurice Ruddick","1950s","Yes","No","Black","Male","Closed now ... So much death. But my, didn't we sing those hymns ... Together?" ], | |
["Jacques Plante","1950s","Yes","No","White","Male","You're a great man, Mr. Plante - standing up to 'em like that." ], | |
["Marshall McLuhan","1960s","Yes","No","White","Male","It's obvious. The medium IS the message." ], | |
["Flags","1960s","Yes","No","White","Male","" ], | |
["Expo 67","1960s","No","Yes","White","Male","" ], | |
["Nat Taylor","1950s","Yes","No","White","Male","" ], | |
["Water Pump","1980s","No","Yes","White","Both","Well, maybe today's technology is the problem." ], | |
["Nellie McClung","1910s","Yes","No","White","Female","Madam Speaker, take it from me ... Nice men don't want the vote." ], | |
["Louis Riel","1880s","Yes","No","Indigenous","Male","We have a right to God's lands. ... Perhaps I am a prophet. I've suffered enough." ], | |
["Maple Leaf Gardens","1920s","No","No","White","Male","" ], | |
["Andrew Mynarski","1940s","Yes","No","White","Male","" ], | |
["Mona Parsons","1940s","Yes","No","White","Female","Gentlemen, good morning." ], | |
["Tommy Prince","1970s","Yes","No","Indigenous","Male","" ], | |
["Vimy Ridge","1910s","No","No","White","Male","And Mother, I thought ... We are a nation! This is us!" ], | |
["Osborn of Hong Kong","1940s","Yes","No","White","Male","Grenades! Grenades! Grenaaaaaaaaaaaaaaaaa ...!!" ], | |
["Home from the Wars","1940s","No","Yes","White","Both","Is that any way to treat citizens that have gone through what we have gone through?" ], | |
["Detraze in the Congo","1960s","Yes","No","White","Male","" ], | |
["Juno Beach","1940s","No","No","White","Male","" ], | |
["Richard Pierpoint","1810s","Yes","No","Black","Male","With respect, sir, I was born a free man and I intend to die one." ], | |
["Queenston Heights","1810s","No","Yes","Indigenous","Male","Comrades and brothers, remember the fame of ancient warriors." ], | |
["Winnipeg Falcons","1920s","No","Yes","White","Male","This is the game we've been saving it for. For us. For Canada." ], | |
["Terry Fox","1980s","Yes","No","White","Male","I want to set an example that'll never be forgotten." ], | |
["Nursing Sisters","1910s","No","Yes","White","Female","If this war doesn't end soon't be a man living on the face of the earth. But don't worry. These nurses continue. I continue." ], | |
["John A. Macdonald","1860s","Yes","No","White","Male","Gentlemen, the time for union is now. I ask you to take the dare!" ], | |
["George-Etienne Cartier","1880s","Yes","No","White","Male","Bold as a lion, Confederation could not have happened but for him." ], | |
["Joseph Tyrell","1880s","Yes","No","White","Male","The blackfoot called them the grandfather of the buffalo. It would prove to be one of the richest dinosaur deposits in the world." ] | |
] |
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
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<body> | |
<style type="text/css"> | |
body { | |
font-family: 'Helvetica Neue', Helvetica, Arial, sans serif; | |
color: #333; | |
line-height: 1.1; | |
} | |
.wrapper { | |
width: 620px; | |
margin: 40px auto; | |
} | |
.gi-heading { | |
font-family: 'Helvetica Neue'; | |
font-weight: bold; | |
font-size: 22px; | |
padding-bottom: 0px; | |
margin-bottom: 12px; | |
} | |
.gi-chart_text { | |
padding: 10px; | |
background-color: #f9f9f9; | |
} | |
.gi-chart_text div { | |
padding: 0; | |
margin: 0; | |
} | |
.gi-legend { | |
text-anchor: middle; | |
font-weight: bold; | |
} | |
.gi-circle:hover { | |
stroke: #CE2A23; | |
stroke-width: 1px; | |
cursor: pointer; | |
} | |
.gi-title, | |
.gi-quote { | |
font-size: 20px; | |
color: #333; | |
} | |
.gi-title { | |
font-weight: bold; | |
} | |
.gi-title:empty:before { | |
content: "Heritage Moment title"; | |
color: #cacaca; | |
} | |
.gi-quote.gi-text_active:empty:before, .gi-title.gi-text_active:empty:before { | |
content: ""; | |
} | |
.gi-quote:empty:before { | |
content: "Quote"; | |
color: #cacaca; | |
} | |
.gi-svg { | |
background-color: #f9f9f9; | |
} | |
.gi-circle { | |
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); | |
} | |
.gi-circle_active { | |
stroke: #CE2A23; | |
stroke-width: 2px; | |
} | |
.gi-selected { | |
stroke: #CE2A23; | |
stroke-width: 2px; | |
} | |
.gi-btngroup { | |
margin-bottom: 16px; | |
} | |
.gi-btngroup_button { | |
font-size: 17px; | |
display: inline-block; | |
padding: 7px; | |
border-radius: 5px; | |
border: 1px solid #333; | |
margin-right: 8px; | |
cursor: pointer; | |
opacity: 0.2; | |
} | |
.gi-btngroup_button:hover { | |
opacity: 0.8; | |
border-color: #CE2A23; | |
color: #CE2A23; | |
} | |
.gi-btngroup_button.gi-btngroup_button-active { | |
opacity: 1; | |
} | |
.gi-footer { | |
text-transform: uppercase; | |
font-size: 12px; | |
margin-top: 4px; | |
} | |
.gi-footer p { | |
margin: 0; | |
} | |
</style> | |
<div class="wrapper"> | |
<div class="gi-heading">All previous Heritage Minutes, by race and gender</div> | |
<div class="gi-btngroup"> | |
</div> | |
<div class="gi-chart_text"> | |
<div class="gi-title"></div> | |
<div class="gi-quote"></div> | |
</div> | |
<div class="gi-chart"></div> | |
</div> | |
<script src="//d3js.org/d3.v3.min.js"></script> | |
<script src="main.js"></script> | |
</body> | |
</html> |
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 data = []; | |
var margin = { | |
top: 10, | |
right: 10, | |
bottom: 10, | |
left: 10 | |
}; | |
var width = d3.select(".gi-chart").node().getBoundingClientRect().width - margin.left - margin.right; | |
var screenScale = d3.scale.linear() | |
.domain([300, 900]) | |
.range([0.6, 0.4]); | |
var dotScale = d3.scale.linear() | |
.domain([300, 620]) | |
.range([0.6, 1]); | |
if (width > screenScale.domain()[1]) { | |
screenScale.domain([300, width]); | |
}; | |
var height = (width * screenScale(width)) - margin.top - margin.bottom, | |
maxRadius = 8 * dotScale(width), | |
padding = 1.5, | |
clusterPadding = 0; | |
var raceExt = [], | |
genderExt = []; | |
var svg = d3.select(".gi-chart").append("svg") | |
.attr({ | |
"width": width + margin.left + margin.right, | |
"height": height + margin.top + margin.bottom, | |
"class": "gi-svg" | |
}) | |
.append("g") | |
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); | |
d3.json("data.json", function(err, jsonData) { | |
if (err) { console.log(err); } | |
var header = jsonData.shift(); | |
jsonData.forEach(function(d, i) { | |
var obj = {}; | |
for (var i = 0; i < d.length; i++) { | |
obj[this[i]] = d[i]; | |
}; | |
data.push(obj); | |
}, header); | |
d3.map(data, function(d) { | |
if (raceExt.indexOf(d.race) < 0) { raceExt.push(d.race); } | |
if (genderExt.indexOf(d.gender) < 0) { genderExt.push(d.gender); } | |
}); | |
var color = d3.scale.ordinal() | |
.domain(raceExt) | |
.range(["#CCC", "#636363", "#CE2A23", "#252525"]); | |
var circleGroup = svg.append("g") | |
.attr({ | |
"transform": "translate(0," + (height * 0.08 * -1) + ")", | |
"class": "gi-circle-group" | |
}); | |
var circle = circleGroup.selectAll("circle").data(data); | |
circle.enter().append("circle") | |
.attr({ | |
"class": "gi-circle", | |
"fill": function(d) { return color(d.race); } | |
}) | |
.on("click", function(d) { | |
d3.selectAll(".gi-circle_active").classed("gi-circle_active", false); | |
d3.select(this).classed("gi-circle_active", true); | |
d3.select(".gi-title") | |
.classed("gi-text_active", true) | |
.text(d.title); | |
var quote = (d.quote !== "") ? "“" + d.quote + "”" : ""; | |
d3.select(".gi-quote") | |
.classed("gi-text_active", true) | |
.html(quote); | |
}); | |
var labelGroup = svg.append("g") | |
.attr("class", "gi-legend"); | |
var buttons = d3.select(".gi-btngroup").selectAll(".gi-btngroup_button") | |
.data(["race", "gender"]).enter() | |
.append("div") | |
.text(function(d) { return d === "race" ? "Race" : "Gender"; }) | |
.attr("class", function(d) { | |
var str = ""; | |
if (d === "race") { str += "gi-btngroup_button-active " } | |
return str += "gi-btngroup_" + d; | |
}) | |
.classed("gi-btngroup_button", true) | |
.on("click", function(d) { | |
buttons.classed("gi-btngroup_button-active", false); | |
d3.select(this).classed("gi-btngroup_button-active", true); | |
updateChart(d) | |
}); | |
var force = d3.layout.force(); | |
updateChart("race"); | |
function updateChart(type) { | |
var extent = (type === "race") ? raceExt : genderExt; | |
var x = d3.scale.ordinal() | |
.domain(d3.range(extent.length)) | |
.rangePoints([0, width], 1); | |
data = data.map(function(d, i) { | |
var i = extent.indexOf(d[type]); | |
d.radius = maxRadius; | |
d.cx = x(i); | |
d.cy = height / 2; | |
return d; | |
}); | |
circle | |
.data(data) | |
.attr("r", function(d) { return d.radius; }) | |
.call(force.drag); | |
force | |
.nodes(data) | |
.size([width, height]) | |
.gravity(0) | |
.charge(0) | |
.on("tick", tick) | |
.start(); | |
labels(extent); | |
} | |
function labels(extent) { | |
svg.selectAll(".gi-legend_text").remove(); | |
var textScale = d3.scale.ordinal() | |
.domain(d3.range(extent.length)) | |
.rangeRoundBands([0, width]); | |
labelGroup.selectAll("text") | |
.data(extent) | |
.enter().append("text") | |
.attr({ | |
"x": function(d, i) { return (textScale.rangeBand() / 2) + (textScale.rangeBand() * i); }, | |
"y": height - (height * 0.1), | |
"class": "gi-legend_text" | |
}) | |
.text(function(d) { return d; }); | |
} | |
function tick(e) { | |
circle | |
.each(gravity(.2 * e.alpha)) | |
.each(collide(.5)) | |
.attr("cx", function(d) { return d.x; }) | |
.attr("cy", function(d) { return d.y; }); | |
} | |
// Move nodes toward cluster focus. | |
function gravity(alpha) { | |
return function(d) { | |
d.y += (d.cy - d.y) * alpha; | |
d.x += (d.cx - d.x) * alpha; | |
}; | |
} | |
// Resolve collisions between nodes. | |
function collide(alpha) { | |
var quadtree = d3.geom.quadtree(data); | |
return function(d) { | |
var r = d.radius + maxRadius + Math.max(padding, clusterPadding), | |
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 : clusterPadding); | |
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; | |
}); | |
}; | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment