|
<!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> |