Skip to content

Instantly share code, notes, and snippets.

@HarryStevens
Last active October 8, 2023 18:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save HarryStevens/302d078a089caf5aeb13e480b86fdaeb to your computer and use it in GitHub Desktop.
Save HarryStevens/302d078a089caf5aeb13e480b86fdaeb to your computer and use it in GitHub Desktop.
Correlation Matrix
license: gpl-3.0

Here's some reusable code to compute and draw a correlation matrix from a set of data in JavaScript. Here's how it works:

  • jeezy's correlationMatrix function calculates a correlation matrix for a set of data.
  • data2grid sets up a layout for that correlation matrix.
  • chroma creates a color scale based on the extents of the correlation matrix (in this case it ignores correlations of 1, because the data set is random and so all correlations are quite low other than when both columns are the same).
  • d3 draws the correlation matrix to the DOM.
<!DOCTYPE html>
<html>
<head>
<style>
body {
margin: 0 auto;
display: table;
font-family: "Helvetica Neue", sans-serif;
}
rect.selected {
stroke: #000;
stroke-width: 2px;
}
.axis .domain {
display: none;
}
.axis .tick text.selected {
font-weight: bold;
font-size: 1.2em;
fill: #47ff63;
}
.axis .tick line.selected {
stroke: #47ff63;
}
.tip {
position: absolute;
font-size: .8em;
text-align: center;
text-shadow: -1px -1px 1px #ffffff, -1px 0px 1px #ffffff, -1px 1px 1px #ffffff, 0px -1px 1px #ffffff, 0px 1px 1px #ffffff, 1px -1px 1px #ffffff, 1px 0px 1px #ffffff, 1px 1px 1px #ffffff;
}
#legend {
margin-bottom: 10px;
}
#legend text {
font-size: .8em;
}
</style>
</head>
<body>
<div id="legend"></div>
<div id="grid"></div>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/jeezy@1.12.11/lib/jeezy.min.js"></script>
<script src="https://unpkg.com/data2grid@1.0.0/build/data2grid.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chroma-js/1.3.5/chroma.min.js"></script>
<script>
d3.select("body").append("div").attr("class", "tip").style("display", "none");
var data = [];
var cols = "abcdefghijklmnopqrstuvwxyz".split("");
for (var i = 0; i <= 30; i++){
var obj = {index: i};
cols.forEach(col => {
obj[col] = jz.num.randBetween(1, 100);
});
data.push(obj);
}
var corr = jz.arr.correlationMatrix(data, cols);
var extent = d3.extent(corr.map(function(d){ return d.correlation; }).filter(function(d){ return d !== 1; }));
var grid = data2grid.grid(corr);
var rows = d3.max(grid, function(d){ return d.row; });
var margin = {top: 20, bottom: 1, left: 20, right: 1};
var dim = d3.min([window.innerWidth * .9, window.innerHeight * .9]);
var width = dim - margin.left - margin.right, height = dim - margin.top - margin.bottom;
var svg = d3.select("#grid").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 + ")");
var padding = .1;
var x = d3.scaleBand()
.range([0, width])
.paddingInner(padding)
.domain(d3.range(1, rows + 1));
var y = d3.scaleBand()
.range([0, height])
.paddingInner(padding)
.domain(d3.range(1, rows + 1));
var c = chroma.scale(["tomato", "white", "steelblue"])
.domain([extent[0], 0, extent[1]]);
var x_axis = d3.axisTop(y).tickFormat(function(d, i){ return cols[i]; });
var y_axis = d3.axisLeft(x).tickFormat(function(d, i){ return cols[i]; });
svg.append("g")
.attr("class", "x axis")
.call(x_axis);
svg.append("g")
.attr("class", "y axis")
.call(y_axis);
svg.selectAll("rect")
.data(grid, function(d){ return d.column_a + d.column_b; })
.enter().append("rect")
.attr("x", function(d){ return x(d.column); })
.attr("y", function(d){ return y(d.row); })
.attr("width", x.bandwidth())
.attr("height", y.bandwidth())
.style("fill", function(d){ return c(d.correlation); })
.style("opacity", 1e-6)
.transition()
.style("opacity", 1);
svg.selectAll("rect")
d3.selectAll("rect")
.on("mouseover", function(d){
d3.select(this).classed("selected", true);
d3.select(".tip")
.style("display", "block")
.html(d.column_x + ", " + d.column_y + ": " + d.correlation.toFixed(2));
var row_pos = y(d.row);
var col_pos = x(d.column);
var tip_pos = d3.select(".tip").node().getBoundingClientRect();
var tip_width = tip_pos.width;
var tip_height = tip_pos.height;
var grid_pos = d3.select("#grid").node().getBoundingClientRect();
var grid_left = grid_pos.left;
var grid_top = grid_pos.top;
var left = grid_left + col_pos + margin.left + (x.bandwidth() / 2) - (tip_width / 2);
var top = grid_top + row_pos + margin.top - tip_height - 5;
d3.select(".tip")
.style("left", left + "px")
.style("top", top + "px");
d3.select(".x.axis .tick:nth-of-type(" + d.column + ") text").classed("selected", true);
d3.select(".y.axis .tick:nth-of-type(" + d.row + ") text").classed("selected", true);
d3.select(".x.axis .tick:nth-of-type(" + d.column + ") line").classed("selected", true);
d3.select(".y.axis .tick:nth-of-type(" + d.row + ") line").classed("selected", true);
})
.on("mouseout", function(){
d3.selectAll("rect").classed("selected", false);
d3.select(".tip").style("display", "none");
d3.selectAll(".axis .tick text").classed("selected", false);
d3.selectAll(".axis .tick line").classed("selected", false);
});
// legend scale
var legend_top = 15;
var legend_height = 15;
var legend_svg = d3.select("#legend").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", legend_height + legend_top)
.append("g")
.attr("transform", "translate(" + margin.left + ", " + legend_top + ")");
var defs = legend_svg.append("defs");
var gradient = defs.append("linearGradient")
.attr("id", "linear-gradient");
var stops = [{offset: 0, color: "tomato", value: extent[0]}, {offset: .5, color: "white", value: 0}, {offset: 1, color: "steelblue", value: extent[1]}];
gradient.selectAll("stop")
.data(stops)
.enter().append("stop")
.attr("offset", function(d){ return (100 * d.offset) + "%"; })
.attr("stop-color", function(d){ return d.color; });
legend_svg.append("rect")
.attr("width", width)
.attr("height", legend_height)
.style("fill", "url(#linear-gradient)");
legend_svg.selectAll("text")
.data(stops)
.enter().append("text")
.attr("x", function(d){ return width * d.offset; })
.attr("dy", -3)
.style("text-anchor", function(d, i){ return i == 0 ? "start" : i == 1 ? "middle" : "end"; })
.text(function(d, i){ return d.value.toFixed(2) + (i == 2 ? ">" : ""); })
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment