|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<style> |
|
|
|
.hexagon { |
|
fill: none; |
|
stroke: #000; |
|
stroke-width: .5px; |
|
} |
|
|
|
.axis text { |
|
font: 10px sans-serif; |
|
} |
|
|
|
.axis path, |
|
.axis line { |
|
fill: none; |
|
stroke: #000; |
|
shape-rendering: crispEdges; |
|
} |
|
|
|
</style> |
|
<body> |
|
<script src="//d3js.org/d3.v4.min.js"></script> |
|
<script src="//d3js.org/d3-hexbin.v0.2.min.js"></script> |
|
<script src="//d3js.org/d3-contour.v1.min.js"></script> |
|
<script> |
|
|
|
var margins = {top: 20, right: 30, bottom: 30, left: 80}; |
|
|
|
var thisClick = function(event) { |
|
if (d3.event.defaultPrevented) return; |
|
|
|
var series = 0; |
|
if (d3.event.ctrlKey) series = 2; |
|
if (d3.event.altKey) series++; |
|
|
|
var point = d3.mouse(this); |
|
var p = [point[0] - margins.left, point[1] - margins.top, series, ptId++]; |
|
|
|
ptData.push(p); |
|
updateVis(); |
|
}; |
|
|
|
var width = 400, |
|
height = 300; |
|
|
|
var xd = [0, 10]; |
|
var yd = [0, 10]; |
|
|
|
var svgIDs = ['binning', 'scatter', 'contour']; |
|
var svgs = d3.select("body").selectAll('svg') |
|
.data(svgIDs).enter() |
|
.append('svg') |
|
.attr('id', d => d) |
|
.attr('width', width) |
|
.attr('height', height); |
|
|
|
var svg = d3.select("svg#binning"); |
|
var scattersvg = d3.select("svg#scatter").on('click', thisClick); |
|
var contoursvg = d3.select("svg#contour"); |
|
|
|
width = +svg.attr('width') - margins.left - margins.right; |
|
height = +svg.attr('height') - margins.top - margins.bottom; |
|
|
|
var x1 = d3.scaleLinear() |
|
.domain(xd) |
|
.range([0, width]); |
|
|
|
var y1 = d3.scaleLinear() |
|
.domain(yd) |
|
.range([height, 0]); |
|
|
|
var hexbin = d3.hexbin() |
|
.size([width,height]) |
|
.radius(10); |
|
|
|
svgs = svgs.append('g') |
|
.attr("transform", "translate(" + margins.left + ", " + margins.top + ")"); |
|
|
|
svgs.append('g') |
|
.attr('class', 'xaxis axis') |
|
.attr('transform', 'translate(0,' + height + ')') |
|
.call(d3.axisBottom().scale(x1)); |
|
|
|
svgs.append('g') |
|
.attr('class', 'yaxis axis') |
|
.call(d3.axisLeft().scale(y1)); |
|
|
|
svgs.append('clipPath') |
|
.attr('id', 'clip') |
|
.append('rect') |
|
.attr('class', 'mesh') |
|
.attr('width', width) |
|
.attr('height', height); |
|
|
|
var colors = d3.schemeCategory10.slice(0, 4); |
|
|
|
var points = scattersvg.select('g').append('g') |
|
.attr('class', 'points'); |
|
|
|
var hexagon = svg.select('g').append("g") |
|
.attr('clip-path', 'url(#clip)') |
|
.attr("class", "hexagons"); |
|
|
|
var contours = contoursvg.select('g').append('g') |
|
.attr('class', 'contours') |
|
.attr('clip-path', 'url(#clip)') |
|
.attr('fill', 'none') |
|
.attr('fill-opacity', 0.3) |
|
.attr('stroke', 'steelblue') |
|
.attr('stroke-width', 2) |
|
.attr('stroke-opacity', 0.7) |
|
.attr('stroke-linejoin', 'round'); |
|
|
|
var attenuation = d3.scaleLog() |
|
.range([0,1]); |
|
|
|
var ptSize = 3; |
|
|
|
// <http://stackoverflow.com/questions/19911514/how-can-i-click-to-add-or-drag-in-d3> |
|
var ptData; |
|
|
|
var updateVis = function() { |
|
var pts = points.selectAll('circle.point') |
|
.data(ptData, function(d) { return d[3]; }); |
|
|
|
pts.exit().remove(); |
|
pts.enter().append('circle') |
|
.attr("class", "point") |
|
.attr('r', ptSize) |
|
.attr('cx', function(e) { return e[0]; }) |
|
.attr('cy', function(e) { return e[1]; }) |
|
.style('fill', function(e) { return colors[e[2]]; }); |
|
|
|
/* hexbins */ |
|
var hexbins = hexbin(ptData); |
|
attenuation.domain([.1, d3.max(hexbins.map(function(d) { return d.length; }))]); |
|
var hex = hexagon.selectAll("path") |
|
.data(hexbins, function(d) { return d.i + "," + d.j }); |
|
|
|
hex.exit().remove(); |
|
hex.enter().append("path") |
|
.attr("d", hexbin.hexagon(9.5)) |
|
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; }) |
|
.merge(hex) |
|
.style("fill", function(d) { |
|
var counts = [0,0,0,0]; |
|
d.forEach(function(p) { |
|
counts[p[2]]++; |
|
}); |
|
|
|
return counts.reduce(function(p, c, i) { |
|
return d3.interpolateRgb(p, colors[i])(c / d.length); |
|
}, "white"); |
|
}).style('opacity', function(d) { return attenuation(d.length); }); |
|
|
|
/* contour */ |
|
// make one group for each class |
|
var grps = [0,1,2,3]; |
|
|
|
// used d3.extent(d3.select("#contour .contours").selectAll('path').data(), d => d.value) post-hoc to find bounds |
|
var colorRange = [0, 0.05]; |
|
|
|
var contourGenerator = d3.contourDensity() |
|
.x(d => d[0]) |
|
.y(d => d[1]) |
|
.size([width, height]) |
|
.thresholds(7) |
|
.bandwidth(10); |
|
|
|
var con = contours.selectAll('g.grp').data(grps); |
|
con.enter().append('g') |
|
.attr('class', 'grp') |
|
.merge(con) |
|
.each(function(grp) { |
|
var thisColor = d3.scaleSequential(d3.interpolateLab("white", colors[grp])) |
|
.domain(colorRange); |
|
|
|
d3.select(this).selectAll('path').remove(); |
|
var con = d3.select(this).selectAll('path') |
|
.data(contourGenerator(ptData.filter(d => d[2] == grp))).enter().append('path') |
|
// .merge(con) |
|
.attr('d', d3.geoPath()) |
|
.attr('stroke', colors[grp]) |
|
.attr('fill', d => thisColor(d.value)); |
|
}); |
|
}; |
|
|
|
var ptId = 0; |
|
d3.csv("cluster-data.csv", function(d) { |
|
return [x1(+d.x), y1(+d.y), +d.category, ptId++]; |
|
}, function(error, rows) { |
|
ptData = rows; |
|
updateVis(); |
|
} |
|
); |
|
|
|
|
|
</script> |