src/matrix.js
/* global $rdf */
// Build a 2D matrix of values
//
// dom AKA document
// query a Query object of rdflib.js with a valid pattern
// vx A variable object, the one to be used for the X variable (horiz)
// vy A variable object, the one to be used for the Y variable (vertical)
// vvalue A variable object, the one to be used for the cell value
// returns A DOM element with the matrix in it, which has a .refresh() function.
//
// Options:
// cellFunction(td, x, y, value) fill the TD element of a single cell
// xDecreasing set true for x axis to be in decreasiong order.
// yDecreasing set true for y axis to be in decreasiong order.
// set_x array of X values to be define initial rows (order irrelevant)
// set_y array of Y values to be define initial columns
//
// Features:
// Header row at top (x axis) and left (y-axis) generated automatically.
// Extra rows and columns are inserted as needed to hold new data points
// matrix.refresh() will re-run the query and adjust the display
var UI = {
icons: require('./iconBase'),
log: require('./log'),
ns: require('./ns'),
pad: require('./'),
rdf: require('rdflib'),
store: require('./store'),
widgets: require('./widgets')
}
const utils = require('./utils')
const kb = UI.store
module.exports.matrixForQuery = function (
dom,
query,
vx,
vy,
vvalue,
options,
whenDone
) {
var matrix = dom.createElement('table')
var header = dom.createElement('tr')
var corner = header.appendChild(dom.createElement('td'))
corner.setAttribute('class', 'MatrixCorner')
matrix.appendChild(header) // just one for now
matrix.lastHeader = header // Element before data
var columns = [] // Vector
var rows = [] // Associative array
var setCell = function (cell, x, y, value) {
while (cell.firstChild) {
// Empty any previous
cell.removeChild(cell.firstChild)
}
cell.setAttribute('style', '')
cell.style.textAlign = 'center'
if (options.cellFunction) {
options.cellFunction(cell, x, y, value)
} else {
cell.textContent = utils.label(value)
cell.setAttribute('style', 'padding: 0.3em')
}
delete cell.old
}
var rowFor = function (y1) {
var y = y1.toNT()
if (rows[y]) return rows[y]
var tr = dom.createElement('tr')
var header = tr.appendChild(dom.createElement('td'))
header.setAttribute('style', 'padding: 0.3em;')
header.textContent = utils.label(y1) // first approximation
if (y1.termType === 'NamedNode') {
kb.fetcher.nowOrWhenFetched(y1.uri.split('#')[0], undefined, function (
ok,
_body,
_response
) {
if (ok) header.textContent = utils.label(y1)
})
}
for (var i = 0; i < columns.length; i++) {
setCell(
tr.appendChild(dom.createElement('td')),
$rdf.fromNT(columns[i]),
y1,
null
)
}
tr.dataValueNT = y
rows[y] = tr
for (var ele = matrix.lastHeader.nextSibling; ele; ele = ele.nextSibling) {
// skip header
if (
(y > ele.dataValueNT && options && options.yDecreasing) ||
(y < ele.dataValueNT && !(options && options.yDecreasing))
) {
return matrix.insertBefore(tr, ele) // return the tr
}
}
return matrix.appendChild(tr) // return the tr
}
var columnNumberFor = function (x1) {
var xNT = x1.toNT() // xNT is a NT string
var col = null
// These are data columns (not headings)
for (var i = 0; i < columns.length; i++) {
if (columns[i] === xNT) {
return i
}
if (
(xNT > columns[i] && options.xDecreasing) ||
(xNT < columns[i] && !options.xDecreasing)
) {
columns = columns
.slice(0, i)
.concat([xNT])
.concat(columns.slice(i))
col = i
break
}
}
if (col === null) {
col = columns.length
columns.push(xNT)
}
// col is the number of the new column, starting from 0
for (var row = matrix.firstChild; row; row = row.nextSibling) {
// For every row header or not
var y = row.dataValueNT
var td = dom.createElement('td') // Add a new cell
td.style.textAlign = 'center'
if (row === matrix.firstChild) {
td.textContent = utils.label(x1)
} else {
setCell(td, x1, $rdf.fromNT(y), null)
}
if (col === columns.length - 1) {
row.appendChild(td)
} else {
var t = row.firstChild
for (var j = 0; j < col + 1; j++) {
// Skip header col too
t = t.nextSibling
}
row.insertBefore(td, t)
}
}
return col
}
var markOldCells = function () {
for (var i = 1; i < matrix.children.length; i++) {
var row = matrix.children[i]
for (var j = 1; j < row.children.length; j++) {
row.children[j].old = true
}
}
}
var clearOldCells = function () {
var row, cell
var colsUsed = []
var rowsUsed = []
if (options.set_y) {
// Knows y values create rows
for (var k = 0; k < options.set_y.length; k++) {
rowsUsed[options.set_y[k]] = true
}
}
if (options.set_x) {
for (k = 0; k < options.set_x.length; k++) {
colsUsed[columnNumberFor(options.set_x[k]) + 1] = true
}
}
for (let i = 1; i < matrix.children.length; i++) {
row = matrix.children[i]
for (let j = 1; j < row.children.length; j++) {
cell = row.children[j]
if (cell.old) {
var y = $rdf.fromNT(row.dataValueNT)
var x = $rdf.fromNT(columns[j - 1])
setCell(cell, x, y, null)
} else {
rowsUsed[row.dataValueNT] = true
colsUsed[j] = true
}
}
}
for (let i = 0; i < matrix.children.length; i++) {
row = matrix.children[i]
if (i > 0 && !rowsUsed[row.dataValueNT]) {
delete rows[row.dataValueNT]
matrix.removeChild(row)
} else {
for (var j = row.children.length - 1; j > 0; j--) {
// backwards
const cell = row.children[j]
if (!colsUsed[j]) {
row.removeChild(cell)
}
}
}
}
var newcolumns = []
for (let j = 0; j < columns.length; j++) {
if (colsUsed[j + 1]) {
newcolumns.push(columns[j])
}
}
columns = newcolumns
}
matrix.refresh = function () {
markOldCells()
kb.query(query, addCellFromBindings, undefined, clearOldCells)
}
var addCellFromBindings = function (bindings) {
var x = bindings[vx]
var y = bindings[vy]
var value = bindings[vvalue]
var row = rowFor(y)
var colNo = columnNumberFor(x)
var cell = row.children[colNo + 1] // number of Y axis headings
setCell(cell, x, y, value)
}
if (options.set_y) {
// Knows y values create rows
for (var k = 0; k < options.set_y.length; k++) {
rowFor(options.set_y[k])
}
}
if (options.set_x) {
for (k = 0; k < options.set_x.length; k++) {
columnNumberFor(options.set_x[k])
}
}
kb.query(query, addCellFromBindings, undefined, whenDone) // Populate the matrix
return matrix
}