This block experiments a way to vizualise the distribution of things (whatever it is) in a horizontal way (ie. along the x-axis), where constraints/objectives are:
This block uses the d3's Force Layout to achieve these objectives. I re-use the technics illustrated in mbostock's block in and later by bjtucker's block, in order to constrain each circle's x-position while allowing vertical accumulation/aggregation of circles.
xxxxxxxxxx
<meta charset="utf-8">
<style>
circle {
stroke-width: 1.5px;
}
line {
stroke: #999;
}
</style>
<body>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.5.1/dat.gui.min.js"></script>
<script>
var width = 960,
height = 500,
radius = 4,
csvData = [],
config = {
manyPoints: false
};
insertControls();
var fill = d3.scale.linear().domain([1,150]).range(['lightgreen', 'pink']);
var force = d3.layout.force()
.gravity(0.2) //higher value mean higher attraction to the center of the force layout; can be replace with 'chargeDistance' (cf. below)
.charge(-(radius*radius-radius)) //higher positive/negative value means higher attraction/repulsion of each circle
//.chargeDistance(radius) //can be used instead of 'gravity' in order to maintain nodes grouped; if not set, chargeDistance is infinite, leading to escaping nodes if there is no gravity and/or friction; but does not re-group escaping nodes, as 'gravity' does
.size([width, height])
.friction(0.7) //higher positive value means easier repositionning
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.append("line")
.attr("x1", 0)
.attr("y1", height/2)
.attr("x2", width)
.attr("y2", height/2)
.style("stroke", "lightgrey");
var tooltip = svg.append("g")
.attr("transform", "translate("+[width/2, 50]+")")
.style("opacity", 0);
var titles = tooltip.append("g").attr("transform", "translate("+[-5,0]+")")
titles.append("text").attr("text-anchor", "end").text("stem(fr):");
titles.append("text")
.attr("text-anchor", "end")
.attr("transform", "translate("+[0,15]+")")
.text("rank:");
titles.append("text")
.attr("text-anchor", "end")
.attr("transform", "translate("+[0,30]+")")
.text("x-value:");
var values = tooltip.append("g").attr("transform", "translate("+[5,0]+")")
var stem = values.append("text");
stem.attr("text-anchor", "start");
var rank = values.append("text");
rank.attr("text-anchor", "start")
.attr("transform", "translate("+[0,15]+")");
var value = values.append("text");
value.attr("text-anchor", "start")
.attr("transform", "translate("+[0,30]+")");
function dottype(d) {
d.stem = d.stem;
d.rank = +d.rank;
d.trend = +d.trend;
d.originalX = width/2+d.trend*6000;
d.x = d.originalX;
d.y = height/2;
csvData.push(d);
return d;
}
d3.csv("data.csv", dottype, function(error, foo) {
if (error) throw error;
draw();
});
function draw () {
var data = copyData(csvData);
if (config.manyPoints) { data = quadruple(data); }
svg.selectAll("circle").remove();
var node = svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("r", radius - .75)
.attr("cx", function(d) { return d.x; })
.attr("cy", height/2)
.style("fill", function(d) { return fill(d.rank); })
.style("stroke", function(d) { return d3.rgb(fill(d.rank)).darker(); })
//.call(force.drag) don't need drag behaviour
.on("mouseenter", function(d) {
stem.text(d.stem);
rank.text(d.rank);
value.text(d.trend);
tooltip.transition().duration(0).style("opacity", 1); // remove fade out transition on mouseleave
})
.on("mouseleave", function(d) {
tooltip.transition().duration(1000).style("opacity", 0);
});
force.nodes(data)
.on("tick", tick)
.stop()
.start();
function tick() {
node.each(function(d){d.x = d.originalX; }) //constrains/fixes x-position
node.attr("cx", function(d) {return d.x = Math.max(radius, Math.min(width - radius, d.x)); })
.attr("cy", function(d) {return d.y = Math.max(radius, Math.min(height - radius, d.y)); });
}
};
function copyData(data) {
return data.map(function(d) {
return {
id: d.id,
stem: d.stem,
rank: d.rank,
trend: d.trend,
originalX: d.originalX,
x: d.originalX,
y: d.y
}
});
};
function quadruple(data) {
// Quadruples data while maintaining order and uniq id
var quadrupledData = [],
i;
data.forEach(function(d) {
for (i=0; i<3; i++) {
quadrupledData.push({
id: d.id+"_"+i,
stem: d.stem,
rank: d.rank,
trend: d.trend,
originalX: d.originalX+i*1E-3,
x: d.x+i*1E-3,
y: d.y
})
}
quadrupledData.push(d);
})
return quadrupledData;
};
function insertControls () {
var ctrls = new dat.GUI({width: 200});
manyPointsCtrl = ctrls.add(config, "manyPoints");
manyPointsCtrl.onChange(function(value) {
draw();
});
};
</script>
</body>
https://d3js.org/d3.v3.min.js
https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.5.1/dat.gui.min.js