Skip to content

Instantly share code, notes, and snippets.

@enjalot
Last active October 28, 2015 19:33
Show Gist options
  • Save enjalot/b41c59f0b6eaf54498c0 to your computer and use it in GitHub Desktop.
Save enjalot/b41c59f0b6eaf54498c0 to your computer and use it in GitHub Desktop.
matrix component

Working towards a reusable matrix component to help illustrate linear algebra concepts.

try scrubbing the numbers on each matrix, notice that the colored cells correspond to each other in the 3x3.

Built with blockbuilder.org

<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
svg { width: 100%; height: 100%; }
.number {
cursor: col-resize;
}
</style>
</head>
<body>
<svg></svg>
<script>
d3.layout.matrix = matrixLayout;
d3.svg.matrix = matrixComponent;
twobytwo = [
[2, -3],
[-3, 1]
];
// our data is a list of rows, which matches the numeric.js format
threebythree = [
[1, 2, 3],
[0, 0, 0],
[3, 1, 2]
]
// we want to link cells to the same value
threemapping = [
["a", "b", "c"],
[ 0, 0, 0],
["c", "a", "b"]
]
// cell size
var size = 30;
var svg = d3.select("svg");
// create a container to hold our first matrix
var twog = svg.append("g")
.attr("transform", "translate(100, 100)")
// instantiate our layout
var twom = new d3.svg.matrix()
.data(twobytwo) // pass in our matrix data
.margin([15, 15])
.cellWidth(size)
.cellHeight(size)
.update(twog) //render the matrix into our group
var threeg = svg.append("g")
.attr("transform", "translate(243, 86)")
// TODO: structure the component so it doesn't need to use "new"
var threem = new d3.svg.matrix()
.data(threebythree)
.mapping(threemapping) // we can define links between cells
.margin([10, 10])
.cellWidth(size+10)
.cellHeight(size)
.update(threeg)
// listen for changes to the data (due to scrubbing)
threem.on("change", function(d, oldData) {
console.log("matrix changed", d, d.data, oldData);
})
var color = d3.scale.category10();
threeg.selectAll("rect.bg").style({
//fill: "#ccfee9",
fill: function(d) {
var m = threemapping[d.i][d.j];
if(m)
return d3.hsl(color(m)).brighter(1.6)
},
rx: 4,
ry: 4
})
function matrixComponent() {
var g;
var data = [[]];
var mapping = [[]];
var nodes = [];
var layout = d3.layout.matrix();
var margin = layout.margin();
var cellWidth = layout.cellWidth();
var cellHeight = layout.cellHeight();
/*
TODO
make scrubbing configurable, per-cell
*/
var dispatch = d3.dispatch("change")
this.update = function(group) {
if(group) g = group;
nodes = layout.nodes(data);
var line = d3.svg.line()
.x(function(d) { return d[0] })
.y(function(d) { return d[1] })
var brackets = g.selectAll("path.brackets")
.data([1, -1])
brackets.enter().append("path")
.attr("d", function(d) {
var nRows = data.length;
var x0 = d * cellWidth/4;
var x1 = -margin[0]/2;
var y0 = -margin[1]/2;
var y1 = (cellHeight + margin[1]) * nRows - margin[1]/2
if(d === 1) {
return line([
[x0, y0],
[x1, y0],
[x1, y1],
[x0, y1]
])
} else {
var dx = (cellWidth + margin[0]) * data[0].length - margin[0]/2
x0 -= margin[0]/2
return line([
[x0 + dx, y0],
[dx, y0],
[dx, y1],
[x0 + dx, y1]
])
}
}).attr({
stroke: "#111",
fill: "none"
})
var cells = g.selectAll("g.number").data(nodes)
var enter = cells.enter().append("g").classed("number", true)
enter.append("rect").classed("bg", true)
cells.select("rect.bg")
.attr({
width: cellWidth,
height: cellHeight,
x: function(d) { return d.x },
y: function(d) { return d.y },
fill: "#fff"
})
enter.append("text")
cells.select("text").attr({
x: function(d) { return d.x + cellWidth/2 },
y: function(d) { return d.y + cellHeight/2 },
"alignment-baseline": "middle",
"text-anchor": "middle",
"line-height": cellHeight,
"fill": "#091242"
}).text(function(d) { return d.data })
var step = 0.1;
var that = this;
var drag = d3.behavior.drag()
.on("drag", function(d) {
var oldData = d.data;
var val = d.data + d3.event.dx * step
val = +(Math.round(val*10)/10).toFixed(1)
set(val, d.i, d.j);
//data[d.i][d.j] = val;
that.update()
dispatch.change(d, oldData)
})
cells.call(drag)
return this;
}
function set(val, i, j) {
var m = mapping[i][j];
if(m){
mapping.forEach(function(row, mi) {
row.forEach(function(col, mj) {
if(col === m) {
data[mi][mj] = val;
}
})
})
}
data[i][j] = val;
}
this.mapping = function(val) {
if(val) {
// TODO make sure dims match
mapping = val;
return this;
}
return mapping;
}
this.data = function(val) {
if(val) {
data = val;
nodes = layout.nodes(data);
return this;
}
return data;
}
this.margin = function(val) {
if(val) {
margin = val;
layout.margin(margin);
return this;
}
return margin;
}
this.cellWidth = function(val) {
if(val) {
cellWidth = val;
layout.cellWidth(cellWidth);
return this;
}
return cellWidth;
}
this.cellHeight = function(val) {
if(val) {
cellHeight = val;
layout.cellHeight(cellHeight);
return this;
}
return cellHeight;
}
d3.rebind(this, dispatch, "on")
return this;
}
function matrixLayout() {
/*
We accept our matrix data as a list of rows:
[ [a, b],
[c, d] ]
*/
var data = [[]];
var nodes;
var margin = [0, 0];
var cellWidth = 20;
var cellHeight = 20;
var nRows;
function getX(i) {
return i * (cellWidth + margin[0])
}
function getY(j) {
return j * (cellHeight + margin[1])
}
function newNodes() {
nRows = data.length;
nodes = [];
data.forEach(function(rows,i) {
rows.forEach(function(col, j) {
var node = {
x: getX(j),
y: getY(i),
data: col,
i: i,
j: j,
index: i * nRows + j
}
nodes.push(node);
})
})
}
function calculate() {
nRows = data.length;
data.forEach(function(rows,i) {
rows.forEach(function(col, j) {
var node = nodes[i * nRows + j];
if(!node) return;
node.data = col;
node.x = getX(j);
node.y = getY(i);
})
})
}
this.nodes = function(val) {
if(val) {
this.data(val);
}
return nodes;
}
this.data = function(val) {
if(val) {
if(val.length === data.length && val[0].length === data[0].length) {
// if the same size matrix is being updated,
// just update the values by reference
// the positions shouldn't change
data = val;
calculate();
} else {
data = val;
newNodes();
}
nRows = data.length;
return this;
}
return data;
}
this.margin = function(val) {
if(val) {
margin = val;
calculate();
return this;
}
return margin;
}
this.cellWidth = function(val) {
if(val) {
cellWidth = val;
calculate();
return this;
}
return cellWidth;
}
this.cellHeight = function(val) {
if(val) {
cellHeight = val
calculate();
return this;
}
return cellHeight;
}
return this;
}
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment