forked from cjhin's block: D3-Force: Split Categorical
forked from Thanaporn-sk's block: D3-Force: Split Categorical
xxxxxxxxxx
<meta charset="utf-8">
<script src="//d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-color.v1.min.js"></script>
<script src="https://d3js.org/d3-interpolate.v1.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script>
d3.csv("data.csv", function(error, data) {
data.forEach(d => {
d.gdp = +d.gdp;
})
////////////////////////
////////////////////////
// Everything unique to this bl.ock is in this function:
function categoricalSplit() {
// Create a scale to translate from categorical (string) data value
// to a point on the screen (effectively an invisible axis)
var catScale = d3.scalePoint()
.domain(data.map(function(d) { return d['continent']; }))
.range([0, width])
.padding(0.5); // give some space at the outer edges
var gdpScale = d3.scaleLinear()
.range([height - 100,height/2 - 50])
.domain(d3.extent(data, d => d['gdp']));
// Add some labels to show whats happening with the split groups
var labels = svg.selectAll("text")
.data(catScale.domain()) // heh, scales take care of the unique, so grab from there
.enter().append("text")
.attr("class", "label")
.text(function(d) { return d; })
.attr("fill", "#DDD")
.attr("text-anchor", "middle")
.attr("x", function(d) { return catScale(d); })
.attr("y", height / 4.0);
var xCatForce = d3.forceX(d => catScale(d['continent']));
var yCatForce = d3.forceY(d => gdpScale(d['gdp']));
var gdpExtent = d3.extent(data,d=>d['gdp']);
var gdpColor = d3.scaleSequential(d3.interpolatePiYG)
.domain(d3.extent(data,d=>d['gdp']));
var gdpMin = gdpExtent[0];
var gdpStep = (gdpExtent[1] - gdpExtent[0])/10;
var gdpLegend = svg.append('g')
.attr('class', 'gdpLegend')
.attr('transform', 'translate(' + [(width/2 + (margin.left*7)), (margin.top)*3] + ')');
var legendTitle = gdpLegend.append('text')
.text('GDP Scale')
.attr('x', 200)
.attr('y', -10)
.style('font-size', '20px')
.attr('fill', '#fff')
var legendMinText = gdpLegend.append('text')
.text(gdpExtent[0])
.attr('x', 200)
.attr('y', -5)
.attr('fill', '#fff');
var legendMaxText = gdpLegend.append('text')
.text(gdpExtent[1])
.attr('x', 200)
.attr('y', -5)
.attr('fill', '#fff');
var legendRects = gdpLegend.selectAll('rect.gdpRect')
.data(new Array(10))
.enter().append('rect')
.attr('class', 'gdpRect')
//.attr('x', (d,i) => i*23)
.attr('x', 9*23)
.attr('y', 0)
.attr('width', 20)
.attr('height', 20)
.attr('fill', '#fff')
// Interaction with button
var splitState = false;
document.getElementById("split-button").onclick = function() {
if(!splitState) {
// push the nodes towards respective spots
simulation.force("x", xCatForce);
simulation.force('y', yCatForce);
d3.selectAll('circle').transition()
.attr('fill', d => gdpColor(d.gdp))
// emphasize labels
labels.attr("fill", "#000");
legendTitle.transition()
.attr('x', 3*23)
.attr('fill', '#000')
legendMinText.transition()
.attr('x', -20)
.attr('fill', '#000')
legendMaxText.transition()
.attr('fill', '#000')
gdpMin = gdpExtent[0];
legendRects.transition()
.attr('x', (d,i) => i*23)
.attr('fill', d => {
var fillColor = gdpColor(gdpMin);
gdpMin = gdpMin + gdpStep;
return fillColor;
});
} else {
// reset
simulation.force("x", centerXForce);
simulation.force('y', centerYForce);
labels.attr("fill", "#DDD");
d3.selectAll('circle').transition()
.attr('fill', '#777')
legendTitle.transition()
.attr('x', 200)
.attr('fill', '#fff')
legendMinText.transition()
.attr('x', 200)
.attr('fill', '#fff')
legendMaxText.transition()
.attr('fill', '#fff')
legendRects.transition()
.attr('x', 9*23)
.attr('fill', '#fff')
}
// Toggle state
splitState = !splitState;
// NOTE: Very important to call both alphaTarget AND restart in conjunction
// Restart by itself will reset alpha (cooling of simulation)
// but won't reset the velocities of the nodes (inertia)
simulation.alpha(1).restart();
};
}
////////////////////////
////////////////////////
////////////////////////
// The rest of this file is from:
// https://bl.ocks.org/cjhin/4c990c57b9b05e58d56b396751f9747d
var margin = {left: 20, top: 20, right: 20, bottom: 20},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var svg = d3.select("svg")
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + [margin.left, margin.right] + ')');
// "Electric repulsive charge", prevents overlap of nodes
var chargeForce = d3.forceManyBody()
//.strength(10)
// Keep nodes centered on screen
var centerXForce = d3.forceX(width / 2);
var centerYForce = d3.forceY(height / 2);
// Apply default forces to simulation
var simulation = d3.forceSimulation()
.force("charge", chargeForce)
.force("x", centerXForce)
.force("y", centerYForce);
var node = svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("r", 10)
.attr("fill", '#777');
// Add the nodes to the simulation, and specify how to draw
simulation.nodes(data)
.on("tick", function() {
// The d3 force simulation updates the x & y coordinates
// of each node every tick/frame, based on the various active forces.
// It is up to us to translate these coordinates to the screen.
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
// Call the function unique to this block
categoricalSplit();
});
</script>
<style>
html {
font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
}
#split-button{
position: absolute;
bottom: 10px;
right: 10px;
padding: 10px 20px;
font-size: 2em;
text-align: center;
background: #FFF;
border-radius: 5px;
border: 1px solid #DDD;
}
#split-button:hover {
background: #CCC;
cursor: pointer;
}
</style>
<body>
<div id="split-button">Toggle Split</div>
<svg></svg>
</body>
https://d3js.org/d3.v4.min.js
https://d3js.org/d3-color.v1.min.js
https://d3js.org/d3-interpolate.v1.min.js
https://d3js.org/d3-scale-chromatic.v1.min.js