Uses nested selections to build the tables. Normally areas/lines/dots/labels would be in their own groups, but grouping the visualization pieces by series allows us to do a really easy hover animation. Unfortunately, we can't control the ordering well this way, so lines that cross get hidden. Data shows the new alpha heat mechanic put in place by PGI in MechWarrior Online. mwomercs.com
Last active
December 20, 2015 06:29
-
-
Save Kusmeroglu/6086007 to your computer and use it in GitHub Desktop.
Nested selectors and multiple series area chart in D3.js
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
<html> | |
<head> | |
<title>Alpha Heat Penalty Graphs</title> | |
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script> | |
<style> | |
body { | |
font: 12px sans-serif; | |
} | |
.axis path, | |
.axis line { | |
fill: none; | |
stroke: #000; | |
shape-rendering: crispEdges; | |
} | |
.line { | |
fill: none; | |
} | |
.hide { | |
visibility: hidden; | |
} | |
.selected { | |
text-decoration: underline; | |
} | |
tr:nth-child(2n) { | |
background-color: #dddddd; | |
} | |
td { | |
padding: 5px 10px; | |
min-width: 45px; | |
} | |
td.heading { | |
font-weight: bold; | |
} | |
p { | |
font: 16px sans-serif; | |
} | |
</style> | |
</head> | |
<body> | |
<h1>Missile Weapons</h1> | |
<div id="heatAlphaGraphMissile"> | |
</div> | |
<div id="heatAlphaGraphMissileTable"> | |
</div> | |
<br/> | |
<h1>Energy Weapons</h1> | |
<div id="heatAlphaGraphEnergy"> | |
</div> | |
<div id="heatAlphaGraphEnergyTable"> | |
</div> | |
<br/> | |
<h1>Ballistic Weapons</h1> | |
<div id="heatAlphaGraphBallistic"> | |
</div> | |
<div id="heatAlphaGraphBallisticTable"> | |
</div> | |
<script> | |
var heatscale = [0.0, 0.08, 0.18, 0.30, 0.45, 0.60, 0.80, 1.10, 1.50, 2.0, 3.0, 5.0]; | |
var ballisticheatdata = [ | |
{name: "AC/20", heat: 6, penaltyheat: 24, penaltynumber: 1}, | |
{name: "AC/10", heat: 3, penaltyheat: 0, penaltynumber: null}, | |
{name: "LB10X - AC/2", heat: 2, penaltyheat: 24, penaltynumber: null}, | |
{name: "GAUSS - AC/5 - UAC/5", heat: 1, penaltyheat: 24, penaltynumber: null} | |
]; | |
var energyheatdata = [ | |
{name: "ERPPC", heat: 11, penaltyheat: 4.5, penaltynumber: 2}, | |
{name: "PPC", heat: 8, penaltyheat: 7.0, penaltynumber: 2}, | |
{name: "ERLL", heat: 9.5, penaltyheat: 2.8, penaltynumber: 2}, | |
{name: "LPLS", heat: 8.5, penaltyheat: 2.8, penaltynumber: 2}, | |
{name: "LLAS", heat: 7, penaltyheat: 2.8, penaltynumber: 2}, | |
{name: "MPLS", heat: 5, penaltyheat: 1.0, penaltynumber: null}, | |
{name: "MLAS", heat: 4, penaltyheat: 1.0, penaltynumber: 6}, | |
{name: "SPLS", heat: 2.4, penaltyheat: 1.0, penaltynumber: null}, | |
{name: "SLAS", heat: 2, penaltyheat: 1.0, penaltynumber: null}, | |
{name: "FLMR", heat: 1, penaltyheat: 1.0, penaltynumber: null} | |
]; | |
var missileheatdata = [ | |
{name: "LRM20", heat: 6, penaltyheat: 2.8, penaltynumber: 2}, | |
{name: "LRM15", heat: 5, penaltyheat: 2.8, penaltynumber: 2}, | |
{name: "LRM10", heat: 4, penaltyheat: 2.8, penaltynumber: 2}, | |
{name: "SRM6", heat: 4, penaltyheat: 1.0, penaltynumber: 3}, | |
{name: "SRM4", heat: 3, penaltyheat: 1.0, penaltynumber: 4}, | |
{name: "SRM2 - SSRM", heat: 2, penaltyheat: 1.0, penaltynumber: 4}, | |
{name: "LRM5", heat: 2, penaltyheat: 2.8, penaltynumber: null} | |
]; | |
// Calculates the heat for a given weapon object and number of weapons fired. | |
var heatfunc = function (d, i) { | |
var heat = 0; | |
for (var n = 1; n <= i; n++) { | |
heat += d.heat; | |
if (d.penaltynumber && n > d.penaltynumber) { | |
heat += (d.heat * (heatscale[n - 1] * d.penaltyheat)); | |
} | |
} | |
//console.log("Weapon: " + d.name + " fired " + i + " times creates " + heat + " heat."); | |
return heat; | |
}; | |
function buildChart(id, heatdata) { | |
var margin = {top: 20, right: 30, bottom: 30, left: 30}, | |
width = 960 - margin.left - margin.right, | |
height = 500 - margin.top - margin.bottom; | |
// create our scales | |
var x = d3.scale.linear() | |
.range([0, width]) | |
.domain([1, 9]); | |
var y = d3.scale.linear() | |
.range([height, 0]) | |
.domain([0, 75]); | |
var color = d3.scale.category10(); | |
color.domain([1, 10]); | |
// create the axis | |
var xAxis = d3.svg.axis() | |
.scale(x) | |
.orient("bottom"); | |
var yAxis = d3.svg.axis() | |
.scale(y) | |
.orient("left"); | |
// define a line and area generation | |
var line = d3.svg.line() | |
.x(function (d) { | |
return x(d.count); | |
}) | |
.y(function (d) { | |
return y(d.heat); | |
}); | |
var area = d3.svg.area() | |
.x(function (d) { | |
return x(d.count); | |
}) | |
.y0(height) | |
.y1(function (d) { | |
return y(d.heat); | |
}); | |
// calculate the values for each weapon at each weapon count (currently up to nine, | |
// the max number you can fit on any mech. | |
heatdata.forEach(function (weaponObj) { | |
var heatnumbers = []; | |
d3.range(1, 10).forEach(function (i) { | |
heatnumbers.push({count: i, heat: heatfunc(weaponObj, i)}); | |
}); | |
weaponObj['values'] = heatnumbers; | |
}); | |
// Create our SVG for the graph | |
var svg = d3.select("#" + id).append("svg") | |
.attr("width", width + margin.left + margin.right) | |
.attr("height", height + margin.top + margin.bottom) | |
.append("g") | |
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); | |
// Create the X Axis | |
svg.append("g") | |
.attr("class", "x axis") | |
.style("stroke-width", "2px") | |
.style("font-weight", "bold") | |
.attr("transform", "translate(0," + (height + 1) + ")") | |
.call(xAxis); | |
// Create the Y Axis | |
svg.append("g") | |
.attr("class", "y axis") | |
.attr("transform", "translate(-1,0)") | |
.style("stroke-width", "2px") | |
.style("font-weight", "bold") | |
.call(yAxis) | |
// add the label | |
.append("text") | |
.attr("transform", "rotate(-90)") | |
.attr("y", 6) | |
.attr("dy", ".71em") | |
.style("text-anchor", "end") | |
.text("Heat"); | |
// Create a group for each weapon | |
var weapongroups = svg.selectAll(".weapongroup") | |
.data(heatdata) | |
.enter() | |
.append("g") | |
.attr("class", "weapongroup").on("mouseover", function () { | |
d3.select(this).selectAll(".valuelabel").classed("hide", false); | |
d3.select(this).selectAll(".label").classed("selected", true); | |
}) | |
.on("mouseout", function () { | |
d3.select(this).selectAll(".valuelabel").classed("hide", true); | |
d3.select(this).selectAll(".label").classed("selected", false); | |
}); | |
// Create the area filling under the line. We add this first so it's on the bottom | |
// NOTE This isn't safe to run over and over on the same graph. Not updateable. | |
weapongroups.append("g") | |
.attr("class", "weaponarea") | |
.append("path") | |
.attr("class", "area") | |
.attr("d", function (d) { | |
return area(d['values']); | |
}) | |
.style("fill", function (d, i) { | |
// halfway between line color and white | |
return d3.interpolateRgb(d3.rgb("#ffffff"), d3.rgb(color(i)))(.5).toString(); | |
}) | |
.style("fill-opacity", 1); | |
// Create the line | |
weapongroups.append("g") | |
.attr("class", "weapon") | |
.append("path") | |
.attr("class", "line") | |
.attr("d", function (d) { | |
return line(d['values']); | |
}) | |
.style("stroke", function (d, i) { | |
return color(i); | |
}); | |
// add the points, mapping a point to each set in values | |
// using a nested selection | |
weapongroups.selectAll(".point") | |
.data(function (d) { | |
return d['values'] | |
}) | |
.enter().append("circle") | |
.attr("class", "point") | |
// j is the parent element's index. fancy! | |
.attr("stroke", function (d, i, j) { | |
return color(j) | |
}) | |
.attr("fill", function (d, i, j) { | |
return color(j) | |
}) | |
.attr("cx", function (d) { | |
return x(d.count) | |
}) | |
.attr("cy", function (d) { | |
return y(d.heat) | |
}) | |
.attr("r", 3); | |
// add the labels for each point, mapping a point to each set in values | |
weapongroups.selectAll(".valuelabel") | |
.data(function (d) { | |
return d['values'] | |
}) | |
.enter().append("text") | |
.attr("class", "valuelabel hide") | |
.attr("stroke", "black") | |
.attr("fill", "black") | |
.attr("x", function (d, i) { | |
return x(d.count) + 4 | |
}) | |
.attr("y", function (d, i) { | |
return y(d.heat) - 4 | |
}) | |
.text(function (d) { | |
return d3.round(d.heat, 2); | |
}); | |
// Legend Labels | |
weapongroups.append("text") | |
.attr("class", "label") | |
.attr("transform", function (d, i) { | |
return "translate(" + 45 + "," + (20 + (i * 30)) + ")"; | |
}) | |
.attr("dy", ".35em") | |
.attr("fill", function (d, i) { | |
return color(i); | |
}) | |
.attr("font-weight", "bold") | |
.text(function (d) { | |
return d.name; | |
}); | |
// make the table, adding a header row | |
var table = d3.select("#" + id + "Table").append("table") | |
var tablerows = table.selectAll("tr") | |
.data(["header"].concat(heatdata)) | |
.enter() | |
.append("tr"); | |
// another nested selection to set up each td | |
tablerows.selectAll("td") | |
.data(function (d) { | |
if (d == "header") { | |
return [""].concat(d3.range(1, 10)); | |
} | |
return [d.name].concat(d['values']); | |
}) | |
.enter() | |
.append("td") | |
.attr("class", function (d) { | |
return d.heat ? "data" : "heading"; | |
}) | |
.text(function (d) { | |
return d.heat ? d3.round(d.heat, 2) : d; | |
}); | |
} | |
buildChart("heatAlphaGraphMissile", missileheatdata); | |
buildChart("heatAlphaGraphEnergy", energyheatdata); | |
buildChart("heatAlphaGraphBallistic", ballisticheatdata); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment