|
<!DOCTYPE html> |
|
<head> |
|
<meta charset="utf-8"> |
|
<script src="https://d3js.org/d3.v4.min.js"></script> |
|
<style> |
|
body { margin:0;top:0;right:0;bottom:0;left:0; font-family: sans-serif; } |
|
.domain { display: none; } |
|
.tick text { fill: #808080 } |
|
.axis-label { fill: #808080; font-size: 12; } |
|
.user-label { font-size: 10px } |
|
.tick line { stroke: #808080; shape-rendering: crispEdges; } |
|
.hull { stroke:#808080; stroke-width: 20; stroke-linejoin: round;} |
|
.legend rect, .legend text { cursor: pointer; } |
|
.gridline { stroke: #808080; shape-rendering: crispEdges; } |
|
</style> |
|
</head> |
|
|
|
<body> |
|
<select id="user"></select> |
|
|
|
<div id="chart"></div> |
|
|
|
|
|
|
|
<script> |
|
|
|
var maxVideoViews; |
|
var maxSubscribers; |
|
var radius = 2; |
|
var mutedOpacity = 0.05; |
|
var xScale1, xScale2; |
|
var yScale1, yScale2; |
|
var xAxisCutOff = 4000000; |
|
var yAxisCutOff = 1000000000; |
|
|
|
function formatBillions(d) { |
|
var roundedNumber = (d/1000000000); |
|
return roundedNumber + "bn"; |
|
}; |
|
|
|
function formatMillions(d) { |
|
var roundedNumber = (d/1000000); |
|
return roundedNumber + "m"; |
|
}; |
|
|
|
d3.csv("data.csv", convertTextToNumbers, function(error, data) { |
|
if(error) { throw error; }; |
|
|
|
var dropDown = d3.select("#user").on("change", highlightUser); |
|
|
|
dropDown.selectAll("option") |
|
.data(data) |
|
.enter() |
|
.append("option") |
|
.attr("value", function(d) { return d.User; }) |
|
.text(function(d) { return d.User; }); |
|
|
|
maxVideoViews = d3.max(data, function(d){ return d.VideoViews; }); |
|
maxSubscribers = d3.max(data, function(d){ return d.Subscribers; }); |
|
|
|
drawChart(data); |
|
|
|
}); |
|
|
|
|
|
|
|
function highlightUser() { |
|
|
|
var sel = document.getElementById('user'); |
|
var selectedUser = sel.options[sel.selectedIndex].value; |
|
|
|
d3.selectAll("circle") |
|
.style("opacity", function(d) { return d.User==selectedUser ? 1 : 0.2; }) |
|
.attr("r", function(d) { return d.User==selectedUser ? 5 : radius; }); |
|
|
|
|
|
|
|
}; |
|
|
|
function drawChart(data) { |
|
|
|
var width = 800; |
|
var height = width; |
|
var margin = {"top": 50, "left": 50, "right": 150, "bottom": 50} |
|
|
|
|
|
var svg = d3.select("#chart").append("svg") |
|
.attr("width", width + margin.right + margin.left) |
|
.attr("height", height + margin.top + margin.bottom); |
|
|
|
var colorScale = d3.scaleOrdinal() |
|
.domain(["A-plus", "A", "A-minus", "B-plus", "B", "B-minus", "C-and-D"]) |
|
.range(["#081d58", "#253494", "#225ea8", "#1d91c0", "#41b6c4", "#7fcdbb", "#c7e9b4"]); |
|
|
|
xScale = d3.scaleLinear() |
|
.range([0,width/2,width]) |
|
.domain([0,xAxisCutOff,maxSubscribers]); |
|
|
|
yScale = d3.scaleLinear() |
|
.range([height,height/2, 0]) |
|
.domain([0,yAxisCutOff,maxVideoViews]); |
|
|
|
var xAxis = d3.axisBottom(xScale).tickFormat(formatMillions); |
|
var yAxis = d3.axisLeft(yScale).tickFormat(formatBillions); |
|
|
|
svg.append("g") |
|
.attr("transform", "translate(" + (margin.left) + "," + (margin.top + height) + ")") |
|
.attr("class", "x-axis") |
|
.call(xAxis); |
|
|
|
svg.append("g") |
|
.attr("transform", "translate(" + (margin.left) + "," + margin.top + ")") |
|
.attr("class", "y-axis") |
|
.call(yAxis); |
|
|
|
/* Simplified based on https://twitter.com/adamrpearce suggestion. Thanks! |
|
xScale1 = d3.scaleLinear() |
|
.range([0,width/2]) |
|
.domain([0,xAxisCutOff]); |
|
|
|
xScale2 = d3.scaleLinear() |
|
.range([width/2, width]) |
|
.domain([xAxisCutOff,maxSubscribers]); |
|
|
|
yScale1 = d3.scaleLinear() |
|
.range([height, height/2]) |
|
.domain([0,yAxisCutOff]); |
|
|
|
yScale2 = d3.scaleLinear() |
|
.range([height/2, 0]) |
|
.domain([yAxisCutOff,maxVideoViews]); |
|
|
|
|
|
var xAxis1 = d3.axisBottom(xScale1).tickFormat(formatMillions); |
|
var xAxis2 = d3.axisBottom(xScale2).tickFormat(formatMillions); |
|
var yAxis1 = d3.axisLeft(yScale1).tickFormat(formatBillions); |
|
var yAxis2 = d3.axisLeft(yScale2).tickFormat(formatBillions); |
|
|
|
svg.append("g") |
|
.attr("transform", "translate(" + (margin.left) + "," + (margin.top + height) + ")") |
|
.attr("class", "x-axis") |
|
.call(xAxis1); |
|
|
|
svg.append("g") |
|
.attr("transform", "translate(" + (margin.left) + "," + (margin.top + height) + ")") |
|
.attr("class", "x-axis") |
|
.call(xAxis2); |
|
|
|
svg.append("g") |
|
.attr("transform", "translate(" + (margin.left) + "," + margin.top + ")") |
|
.attr("class", "y-axis") |
|
.call(yAxis1); |
|
|
|
svg.append("g") |
|
.attr("transform", "translate(" + (margin.left) + "," + margin.top + ")") |
|
.attr("class", "y-axis") |
|
.call(yAxis2); |
|
*/ |
|
|
|
svg.selectAll(".y-axis").selectAll("line").filter(function(d) { return d == yAxisCutOff; }).remove(); |
|
|
|
svg.selectAll(".x-axis").selectAll("line").filter(function(d) { return d == xAxisCutOff; }).remove(); |
|
|
|
|
|
var g = svg.append("g") |
|
.attr("transform", "translate(" + (margin.left) + "," + margin.top + ")"); |
|
|
|
g.append("line") |
|
.attr("class", "gridline") |
|
.attr("x1", xScale(xAxisCutOff)) |
|
.attr("y1", 0) |
|
.attr("x2", xScale(xAxisCutOff)) |
|
.attr("y2", height) |
|
|
|
g.append("line") |
|
.attr("class", "gridline") |
|
.attr("x1", 0) |
|
.attr("y1", yScale(yAxisCutOff)) |
|
.attr("x2", width) |
|
.attr("y2", yScale(yAxisCutOff)) |
|
|
|
g.append("text") |
|
.text("Views (billions)") |
|
.attr("class", "axis-label") |
|
.attr("x", -margin.left/2) |
|
.attr("y", yScale(maxVideoViews)); |
|
|
|
g.append("text") |
|
.text("Subscribers (millons)") |
|
.attr("class", "axis-label") |
|
.attr("x", xScale(maxSubscribers)) |
|
.attr("y", yScale(50000000)) |
|
.style("text-anchor", "end"); |
|
|
|
var nestedData = d3.nest() |
|
.key(function(d){ return d.Rating; }) |
|
.sortKeys(d3.ascending) |
|
.entries(data); |
|
|
|
nestedData.forEach(function(d) { |
|
|
|
var ratingData = []; |
|
|
|
d.values.forEach(function(d) { |
|
ratingData.push([xScale(d.Subscribers),yScale(d.VideoViews)]) |
|
}) |
|
|
|
var hullData = ratingData.length == 2 ? ratingData : d3.polygonHull(ratingData); |
|
|
|
var hull = g.append("path") |
|
.attr("class", "hull") |
|
.attr("id", "hull-" + d.key) |
|
.datum(hullData) |
|
.attr("d", function(d) { return "M" + d.join("L") + "Z"; }) |
|
.style("fill", colorScale(d.key)) |
|
.style("stroke", colorScale(d.key)) |
|
.style("opacity", mutedOpacity); |
|
|
|
}); |
|
|
|
var circles = g.selectAll("g") |
|
.data(data) |
|
.enter() |
|
.append("g") |
|
.attr("class", "user-circle") |
|
.attr("transform", function(d) { return "translate(" + xScale(d.Subscribers) + "," + yScale(d.VideoViews) +")"; }); |
|
|
|
circles.append("circle") |
|
.attr("r", radius) |
|
.attr("cx", 0) |
|
.attr("cy", 0) |
|
.style("fill", function(d) { return colorScale(d.Rating); }) |
|
.style("fill-opacity", 0.8 ); |
|
|
|
circles.append("text") |
|
.text(function(d){ return d.User; }) |
|
.attr("class", "user-label") |
|
.attr("x", 0) |
|
.attr("y", -5) |
|
.attr("text-anchor", "middle") |
|
.style("visibility", function(d){ |
|
if (d.Rating == "A-plus" || d.Subscribers > 12000000 || d.VideoViews > 4000000000) { |
|
return "inherit"; |
|
} else { |
|
return "hidden"; |
|
}; |
|
}); |
|
|
|
d3.selectAll("circle") |
|
.on("mouseover", function(d){ |
|
d3.select("#user").property('value', d.User); |
|
highlightUser(); |
|
}) |
|
.on("mouseout", function(d){ |
|
d3.selectAll("circle").style("opacity", 0.7).attr("r", radius); |
|
}) |
|
|
|
var legendBoxWidth = 20; |
|
|
|
var legend = svg.append("g") |
|
.attr("class", "legend") |
|
.attr("transform", "translate(" + (margin.left + xScale(200000)) + "," + (margin.top + yScale(8000000000)) + ")"); |
|
|
|
|
|
legend.append("text") |
|
.text("Click to highlight the ratings:") |
|
.attr("x", 0) |
|
.attr("y", -5); |
|
|
|
|
|
/*legend.append("rect") |
|
.attr("class", "legend-border") |
|
.attr("x", -3) |
|
.attr("y", -25) |
|
.attr("width", margin.right - 50) |
|
.attr("height", 180) |
|
.style("fill", "none") |
|
.style("stroke", "grey")*/ |
|
|
|
var legendEntries = legend.selectAll("g") |
|
.data(["A-plus", "A", "A-minus", "B-plus", "B", "B-minus", "C-and-D"]) |
|
.enter() |
|
.append("g") |
|
.attr("transform", function(d, i) { return "translate(0," + (i * (legendBoxWidth + 2)) + ")"; }); |
|
|
|
legendEntries.append("rect") |
|
.attr("x", 0) |
|
.attr("y", 0) |
|
.attr("width", legendBoxWidth) |
|
.attr("height", legendBoxWidth) |
|
.attr("class", "legend-rect") |
|
.attr("id", function(d) { return "legend-rect-" + d; }) |
|
.style("fill", function(d) { return colorScale(d); }) |
|
.style( |
|
"opacity", mutedOpacity) |
|
.on("click", showHideHull); |
|
|
|
legendEntries.append("text") |
|
.text(function(d) { return d; }) |
|
.attr("x", legendBoxWidth + 3) |
|
.attr("y", legendBoxWidth - 3) |
|
.on("click", showHideHull); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
function convertTextToNumbers(d) { |
|
d.Rank = +d.Rank; |
|
d.Subscribers = +d.Subscribers; |
|
d.VideoViews = +d.VideoViews; |
|
return d; |
|
}; |
|
|
|
function showHideHull(d) { |
|
|
|
var hullID = "#hull-" + d; |
|
var rectID = "#legend-rect-" + d; |
|
|
|
var currentOpacity = d3.select(hullID).style("opacity"); |
|
|
|
//if (currentOpacity == 0) { |
|
|
|
//selectedHullOpacity = 0.6; |
|
|
|
//d3.select(hullID).style("opacity", selectedHullOpacity); |
|
//d3.select(rectID).style("opacity", selectedHullOpacity); |
|
|
|
//} else { |
|
var selectedHullOpacity = currentOpacity == mutedOpacity ? 0.5 : mutedOpacity; |
|
|
|
//var otherHullsOpacity = currentOpacity == mutedOpacity ? 0 : mutedOpacity; |
|
|
|
//d3.selectAll(".hull").style("opacity", otherHullsOpacity); |
|
d3.select(hullID).style("opacity", selectedHullOpacity); |
|
|
|
//d3.selectAll(".legend-rect").style("opacity", otherHullsOpacity); |
|
d3.select(rectID).style("opacity", selectedHullOpacity); |
|
|
|
//}; |
|
|
|
|
|
|
|
|
|
} |
|
|
|
/* |
|
function xScale(v) { |
|
if (v <= xAxisCutOff) { |
|
return xScale1(v); |
|
} else { |
|
return xScale2(v); |
|
} |
|
}; |
|
|
|
|
|
function yScale(v) { |
|
if (v <= yAxisCutOff) { |
|
return yScale1(v); |
|
} else { |
|
return yScale2(v); |
|
} |
|
}; |
|
*/ |
|
</script> |
|
</body> |