Skip to content

Instantly share code, notes, and snippets.

@biovisualize
Created October 28, 2015 21:50
Show Gist options
  • Save biovisualize/92c0875b1ec24654a6be to your computer and use it in GitHub Desktop.
Save biovisualize/92c0875b1ec24654a6be 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

forked from enjalot's block: matrix component

<!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>
var matrix = {utils: {}, layout: {}, component: {}};
matrix.utils = {
merge: function(obj1, obj2){
for(var p in obj2){
if(obj2[p] && obj2[p].constructor == Object){
if(obj1[p]){
this.merge(obj1[p], obj2[p]);
continue;
}
}
obj1[p] = obj2[p];
}
},
mergeAll: function(){
var newObj = {};
for(var i = 0; i < arguments.length; i++){
this.merge(newObj, arguments[i]);
}
return newObj;
}
};
matrix.layout = function(){
/*
We accept our matrix data as a list of rows:
[ [a, b],
[c, d] ]
*/
var config = {
margin: [0, 0],
cellWidth: 20,
cellHeight: 20
};
var data = [[]];
var nodes;
var nRows;
function getX(i) {
return i * (config.cellWidth + config.margin[0])
}
function getY(j) {
return j * (config.cellHeight + config.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);
});
})
}
var exports = {};
exports.nodes = function(val) {
if(val) {
this.data(val);
}
return nodes;
};
exports.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;
};
exports.config = function(val) {
if(val) {
config = matrix.utils.mergeAll(config, val);
calculate();
return this;
}
return matrix.utils.mergeAll({}, config);
};
return exports;
};
matrix.component = function() {
var g;
var data = [[]];
var nodes = [];
var matrixLayout = matrix.layout();
var config = {
container: null,
mapping: [[]]
};
/*
TODO
make scrubbing configurable, per-cell
*/
var dispatch = d3.dispatch("change");
function set(val, i, j) {
var m = config.mapping[i][j];
if(m){
config.mapping.forEach(function(row, mi) {
row.forEach(function(col, mj) {
if(col === m) {
data[mi][mj] = val;
}
})
})
}
data[i][j] = val;
}
var exports = {};
exports.update = function(_data) {
if(config.container) g = config.container;
data = _data;
nodes = matrixLayout.nodes(data);
var layout = matrixLayout.config();
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 * layout.cellWidth/4;
var x1 = -layout.margin[0]/2;
var y0 = -layout.margin[1]/2;
var y1 = (layout.cellHeight + layout.margin[1]) * nRows - layout.margin[1]/2;
if(d === 1) {
return line([
[x0, y0],
[x1, y0],
[x1, y1],
[x0, y1]
])
} else {
var dx = (layout.cellWidth + layout.margin[0]) * data[0].length - layout.margin[0]/2;
x0 -= layout.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: layout.cellWidth,
height: layout.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 + layout.cellWidth/2 },
y: function(d) { return d.y + layout.cellHeight/2 },
"alignment-baseline": "middle",
"text-anchor": "middle",
"line-height": layout.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(data);
dispatch.change(d, oldData)
});
cells.call(drag);
return this;
};
exports.config = function(val) {
if(val) {
config = matrix.utils.mergeAll(config, val);
matrixLayout.config(val);
return this;
}
return matrix.utils.mergeAll({}, config);
};
d3.rebind(exports, dispatch, "on");
return exports;
};
// Usage
//////////////////
var twobytwo = [
[2, -3],
[-3, 1]
];
// our data is a list of rows, which matches the numeric.js format
var threebythree = [
[1, 2, 3],
[0, 0, 0],
[3, 1, 2]
];
// we want to link cells to the same value
var 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 = matrix.component()
.config({
container: twog,
margin: [15, 15],
cellWidth: size,
cellHeight: size
})
.update(twobytwo); //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 = matrix.component()
.config({
container: threeg,
margin: [10, 10],
cellWidth: size + 10,
cellHeight: size,
mapping: threemapping // we can define links between cells
})
.update(threebythree);
//
// 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
});
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment