Skip to content

Instantly share code, notes, and snippets.

@Kusmeroglu
Last active December 20, 2015 06:29
Show Gist options
  • Save Kusmeroglu/6086007 to your computer and use it in GitHub Desktop.
Save Kusmeroglu/6086007 to your computer and use it in GitHub Desktop.
Nested selectors and multiple series area chart in D3.js

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

<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