Skip to content

Instantly share code, notes, and snippets.

@flekschas
Last active February 23, 2016 16:59
Show Gist options
  • Save flekschas/d52c5be411bfe2e66e65 to your computer and use it in GitHub Desktop.
Save flekschas/d52c5be411bfe2e66e65 to your computer and use it in GitHub Desktop.
Caleydo Web Tutorial - AngularJS heatmap integration
var DUMMY_DATA = [['ID','a','b','c','d','e'],
['1', 10, 9.14, 1, 2, 4],
['2', 8, 8.14, 1, 2, 4],
['3', 13, 8.74, 1, 2, 4],
['4', 9, 8.77, 1, 2, 4],
['5', 11, 9.26, 1, 2, 4],
['6', 14, 8.1, 1, 2, 4],
['7', 6, 6.13, 1, 2, 4],
['8', 4, 3.1, 1, 2, 4],
['9', 12, 9.13, 1, 2, 4],
['10', 7, 7.26, 1, 2, 4],
['11', 5, 4.74, 1, 2, 4]];
@import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,700);
$colorBlindBlue: #50b4e9;
$colorBlindYellow: #f0e442;
$colorDirectHoverBg: rgba(0, 0, 0, 0.2);
$colorIndirectHoverBg: rgba(0, 0, 0, 0.1);
$colorSelectedBg: $colorBlindYellow;
#app {
font-family: 'Source Sans Pro', sans-serif;
font-size: 14px;
}
#table {
.ids {
font-weight: bold;
color: #000;
&:hover {
cursor: pointer;
}
&.soft-highlight {
background: $colorIndirectHoverBg;
}
&.selected {
background: lighten($colorSelectedBg, 15%);
}
}
.cell {
color: #333;
&:hover {
color: #000;
background: $colorDirectHoverBg;
box-shadow: none !important;
}
&.soft-highlight-horizontally {
box-shadow: inset 0 2px 0 0 $colorIndirectHoverBg, inset 0 -2px 0 0 $colorIndirectHoverBg;
}
&.soft-select-horizontally {
box-shadow: inset 0 2px 0 0 $colorSelectedBg, inset 0 -2px 0 0 $colorSelectedBg;
}
&.soft-highlight-vertically {
box-shadow: inset 2px 0 0 0 $colorIndirectHoverBg, inset -2px 0 0 0 $colorIndirectHoverBg;
}
&.soft-select-vertically {
box-shadow: inset 2px 0 0 0 $colorSelectedBg, inset -2px 0 0 0 $colorSelectedBg;
}
&.soft-select-horizontally.soft-select-vertically {
box-shadow: inset 0 0 0 2px $colorSelectedBg;
}
&:last-child {
&.soft-highlight-horizontally {
box-shadow: inset 0 2px 0 0 $colorIndirectHoverBg, inset 0 -2px 0 0 $colorIndirectHoverBg, inset -2px 0 0 0 $colorIndirectHoverBg;
}
&.soft-select-horizontally {
box-shadow: inset 0 2px 0 0 $colorSelectedBg, inset 0 -2px 0 0 $colorSelectedBg, inset -2px 0 0 0 $colorSelectedBg;
}
}
&.selected {
background: $colorSelectedBg;
}
}
td, th {
min-width: 2rem;
padding: 0.25rem;
text-align: center;
}
input {
width: 2rem;
text-align: center;
border: none;
background: none;
}
tfoot {
color: #808080;
td.soft-highlight-vertically {
color: #333;
box-shadow: inset 2px 0 0 0 $colorIndirectHoverBg, inset -2px 0 0 0 $colorIndirectHoverBg, inset 0 2px 0 0 $colorIndirectHoverBg, inset 0 -2px 0 0 $colorIndirectHoverBg;
}
td.soft-select-vertically {
color: #333;
box-shadow: inset 2px 0 0 0 $colorSelectedBg, inset -2px 0 0 0 $colorSelectedBg, inset 0 2px 0 0 $colorSelectedBg, inset 0 -2px 0 0 $colorSelectedBg;
}
}
}
#table,
#caleydo-heat-map {
display: block;
float: left;
margin: 1rem;
border: 1px solid $colorIndirectHoverBg;
}
#caleydo-heat-map {
margin-left: 0;
padding: 0.25rem;
svg {
vertical-align: top;
}
.select-selected {
fill: $colorSelectedBg;
}
}
// Remove useless arrows
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
margin: 0;
}
<div id="app" ng-app="matrixApp" ng-controller="matrixAppCtrl as app">
<table id="table" ng-mouseleave="app.leaveTableBody()">
<thead>
<tr>
<th
ng-if="app.matrix.columnIds || app.matrix.rowIds"
ng-mouseenter="app.leaveTableBody()"></th>
<th
ng-class="{ 'soft-highlight': app.hoveringColumn === $index, 'selected': app.columnSelected($index) }"
ng-click="app.selectColumn($index)"
ng-mouseenter="app.enterColumn($index)"
ng-repeat="id in app.matrix.columnIds track by $index" class="ids">{{ app.matrix.columnHeader($index) }}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="row in app.matrix.rowIds track by $index">
<td
class="ids"
ng-class="{ 'soft-highlight': app.hoveringRow === $parent.$index, 'selected': app.rowSelected($parent.$index) }"
ng-if="app.matrix.rowIds.length"
ng-mouseenter="app.enterRow($index)"
ng-click="app.selectRow($index)">{{ app.matrix.rowHeader($index) }}</td>
<td
class="cell"
ng-class="{
'soft-highlight-horizontally': app.hoveringRow === $parent.$index,
'soft-highlight-vertically': app.hoveringColumn === $index,
'selected': app.cellSelected($index, $parent.$index)
}"
ng-repeat="cell in app.matrix.columnIds track by $index"
ng-mouseenter="app.enterCell($parent.$index, $index)"
ng-click="app.selectCell($index, $parent.$index)">
<input
type="number"
min="0"
step="0.01"
ng-model="app.matrix.cell($index, $parent.$index)"
ng-model-options="{ getterSetter: true }" />
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td ng-mouseenter="app.leaveTableBody()">Sum</td>
<td
ng-class="{
'soft-highlight-vertically': app.hoveringColumn === $index,
'soft-select-vertically': app.columnSelected($index)
}"
ng-mouseenter="app.enterColumn($index)"
ng-repeat="sum in app.matrix.columnSums track by $index">{{ sum }}</td>
</tr>
</tfoot>
</table>
<heat-map matrix-promise="app.matrixPromise"></heat-map>
</div>
// Final matrix factory function used by Angular
var matrixFactory = function (stampit, eventStamp, matrixStamp) {
// The final matrix object is composed of the object and matrix factory
// functions.
return stampit.compose(eventStamp, matrixStamp);
};
// Matrix selection service
var matrixSelectionService = function (
stampit, eventStamp, matrixSelectionStamp
) {
// App-wide selection service is initialized only once.
var selections = stampit.compose(eventStamp, matrixSelectionStamp)();
return selections;
};
var apiService = (function () {
var Q;
function api (_q_) {
Q = _q_;
}
api.prototype.getData = function () {
return Q.when(DUMMY_DATA);
};
return api;
}());
// App controller wrapper
var matrixAppCtrl = (function () {
// Private
var API;
var MATRIX;
var MATRIX_SELECTION;
// App controller
function matrixApp (_matrix_, _matrixSelection_, _api_) {
MATRIX = _matrix_;
MATRIX_SELECTION = _matrixSelection_;
API = _api_;
this.matrixPromise = API.getData().then(function (data) {
this.data = data;
this.matrix = MATRIX(null, this.data, true, true);
// Set dimensionality.
MATRIX_SELECTION.dimensions = this.matrix.getDimensionality();
return this.matrix;
}.bind(this));
// Link matrix selection methods.
this.selectCell = MATRIX_SELECTION.selectCell;
this.selectColumn = MATRIX_SELECTION.selectColumn;
this.selectRow = MATRIX_SELECTION.selectRow;
this.cellSelected = MATRIX_SELECTION.cellSelected;
this.columnSelected = MATRIX_SELECTION.columnSelected;
this.rowSelected = MATRIX_SELECTION.rowSelected;
}
// When mouse leaves the table element.
matrixApp.prototype.leaveTableBody = function () {
this.hoveringRow = null;
this.hoveringColumn = null;
};
// When mouse enters a column.
matrixApp.prototype.enterColumn = function (id) {
this.hoveringRow = null;
this.hoveringColumn = id;
};
// When mouse enters a row.
matrixApp.prototype.enterRow = function (id) {
this.hoveringRow = id;
this.hoveringColumn = null;
};
// When mouse enters a cell.
matrixApp.prototype.enterCell = function (rowId, columnId) {
this.hoveringRow = rowId;
this.hoveringColumn = columnId;
};
return matrixApp;
}());
// Heat-map directive controller wrapper
var heatMapCtrl = (function () {
// Private data
var CALEYDO;
var ELEMENT;
var MATRIX_SELECTION;
var OPTIONS = {
color: ['#f0f2f4', '#cc79a7'],
initialScale: 20
};
var _caleydoMatrix;
var _caleydoHeatMap;
// heat-map controller
function heatMap (_element_, _caleydo_, _matrixSelection_) {
// Set "constant" values.
CALEYDO = _caleydo_;
ELEMENT = _element_;
MATRIX_SELECTION = _matrixSelection_;
this.matrixPromise.then(function (matrix) {
this.matrix = matrix;
this.parseMatrix();
this.draw();
this.initEventListeners();
}.bind(this));
}
heatMap.prototype.initEventListeners = function () {
// Listen to Caleydo's selection events.
_caleydoMatrix.on('select', function(event, type, selection) {
var rowIds = selection.dim(0).asList();
var columnIds = selection.dim(1).asList();
console.log(type, columnIds, rowIds);
});
// Listen to Angular's cell selection events.
MATRIX_SELECTION.on('cellSelected', function (selection) {
//_caleydoMatrix.selectProduct(
// [[selection.column],[selection.row]],
// selection.selected ? 1 : 2
//);
});
// Listen to Angular's column selection events.
MATRIX_SELECTION.on('columnSelected', function (selection) {
// Trigger a Caleydo range selection.
// SelectionOperation:
// 1 === ADD
// 2 === REMOVE
_caleydoMatrix.select(
[[],[selection.column]],
selection.selected ? 1 : 2
);
});
// Listen to Angular's row selection events.
MATRIX_SELECTION.on('rowSelected', function (selection) {
// Trigger a Caleydo range selection.
// SelectionOperation:
// 1 === ADD
// 2 === REMOVE
_caleydoMatrix.select(
[[selection.row],[]],
selection.selected ? 1 : 2
);
});
// Refresh heat-map when data changes
this.matrix.on('dataChanged', this.refresh.bind(this));
};
// Draw heat-map
heatMap.prototype.parseMatrix = function () {
// Deep cloning is necessary due to:
// https://github.com/Caleydo/caleydo_web_container/issues/144
_caleydoMatrix = CALEYDO.d3.parser.parseMatrix(
_.cloneDeep(this.matrix.getRawData())
);
}
// Draw heat-map
heatMap.prototype.draw = function () {
_caleydoHeatMap = CALEYDO.vis.heatmap.create(
_caleydoMatrix, ELEMENT[0], OPTIONS
);
}
// Redraw heat-map
heatMap.prototype.refresh = function () {
this.clear();
this.parseMatrix();
this.draw();
}
// Remove all heat-map related DOM elements
heatMap.prototype.clear = function () {
_caleydoHeatMap.destroy();
};
return heatMap;
}());
// Heat-map directive definition
var heatMapDirective = function () {
return {
bindToController: {
matrixPromise: '='
},
controller: 'heatMapCtrl',
controllerAs: 'heatMap',
restrict: 'E',
replace: true,
scope: {
matrixPromise: '='
},
template: '<div id="caleydo-heat-map"><div>'
}
};
/* ---------- Angular Linking ---------- */
angular
.module('matrixApp', [])
.constant('caleydo', window.Caleydo)
.constant('stampit', window.stampit)
.constant('eventStamp', eventStamp)
.constant('matrixStamp', matrixStamp)
.constant('matrixSelectionStamp', matrixSelectionStamp)
.factory('matrix', ['stampit', 'eventStamp', 'matrixStamp', matrixFactory])
.service('api', ['$q', apiService])
.service('matrixSelection', [
'stampit', 'eventStamp', 'matrixSelectionStamp', matrixSelectionService
])
.controller('matrixAppCtrl', ['matrix', 'matrixSelection', 'api', matrixAppCtrl])
.controller('heatMapCtrl', [
'$element', 'caleydo', 'matrixSelection', heatMapCtrl
])
.directive('heatMap', [heatMapDirective]);
name: Caleydo Web Tutorial - AngularJS integration
description: A simple demo on how to integrate Caleydo Web's matrix vis into an AngularJS app.
authors:
- Fritz Lekschas
resources:
- //ajax.googleapis.com/ajax/libs/angularjs/1.5.0/angular.min.js
- //cdnjs.cloudflare.com/ajax/libs/stampit/2.1.0/stampit.min.js
- //cdnjs.cloudflare.com/ajax/libs/d3/3.5.14/d3.min.js
- //cdnjs.cloudflare.com/ajax/libs/lodash.js/4.5.1/lodash.min.js
- //rawgit.com/Caleydo/caleydo_web/02056f5408fc33402f9de79aa75ddfd097188195/bundle.js
- //rawgit.com/flekschas/d52c5be411bfe2e66e65/raw/0abaf084965178bca5a50de52e6dd9394cf8e183/data.js
- //rawgit.com/flekschas/d52c5be411bfe2e66e65/raw/b1465a53e53deaa401ac3eb3a6b2393b5e96f4d4/stamps.js
normalize_css: yes
wrap: h
panel_js: 0
panel_css: 1
// Very simple event factory
var eventStamp = stampit.init(function() {
var stack = {};
this.on = function (event, callback) {
if (event in stack) {
stack[event].push(callback);
} else {
stack[event] = [callback];
}
return this;
};
this.trigger = function (event, payload) {
if (event in stack) {
for (var i = stack[event].length; i--;) {
stack[event][i](payload);
}
}
return this;
};
});
// Generic matrix factory function
var matrixStamp = stampit.init(function(stamp) {
var THAT = stamp.instance;
var data = stamp.args[0];
var columnIds = stamp.args[1];
var rowIds = stamp.args[2];
// Update the column sums
function updateColumnSums () {
var sums = [];
for (
var row = rowIds ? 1 : 0,
numRows = data.length;
row < numRows;
row++
) {
for (
var column = columnIds ? 1 : 0,
numColumns = data[row].length;
column < numColumns;
column++
) {
var realColumn = columnIds ? column - 1 : column;
sums[realColumn] = sums[realColumn] ?
sums[realColumn] + data[row][column] : data[row][column];
}
}
THAT.columnSums = sums;
}
// Initialize column sums
updateColumnSums();
// Generates an empty array of length # columns
// Note that this is barely a helper array to support _ngRepeat_.
this.columnIds = (function () {
return [].constructor(
columnIds && data.length ?
(rowIds ? data[0].length - 1 : data[0].length) : 0
);
}());
// Returns column header entry
this.columnHeader = function (column) {
return columnIds ? data[0][rowIds ? column + 1 : column] : undefined;
};
// Generates an empty array of length # rows
// Note that this is barely a helper array to support _ngRepeat_.
this.rowIds = (function () {
return [].constructor(
rowIds && data.length ? (columnIds ? data.length - 1 : data.length) : 0
);
}());
// Returns row header entry
this.rowHeader = function (row) {
return rowIds ? data[columnIds ? row + 1 : row][0] : undefined;
};
// Proxy that returns a mapped getter / setter function
// This outer function will be called when Angular initialized the template.
this.cell = function (column, row) {
// The inner function will have have access to the specific _column_ and
// _row_ and returns a getter / setter ultimately used by Angular to link
// the model.
return function (value) {
if (typeof value === 'undefined') {
return data[rowIds ? row + 1 : row][columnIds ? column + 1 : column];
} else {
data[rowIds ? row + 1 : row][columnIds ? column + 1 : column] = value;
updateColumnSums();
THAT.trigger('dataChanged', {
rowId: row,
columnId: column,
value: value
});
return value;
}
}
};
// Returns the raw data object containing the column and row headers
this.getRawData = function () {
return data;
};
// Helper method to return the dimensionality
this.getDimensionality = function () {
return {
columns: this.columnIds.length,
rows: this.rowIds.length
};
}
});
// Factory function for matrix selections
var matrixSelectionStamp = stampit.init(function (stamp) {
var THAT = stamp.instance;
var selectedCells = {};
var selectedColumns = {};
var selectedRows = {};
// Helper method to get index for a matrix cell.
function getIndex (column, row) {
return THAT.dimensions.columns * row + column;
}
// Select a single cell.
this.selectCell = function (column, row, forceSelect) {
var index = getIndex(column, row);
selectedCells[index] = selectedCells[index] &&
!forceSelect ? undefined : true;
THAT.trigger('cellSelected', {
row: row,
column: column,
selected: selectedCells[index]
});
};
// Select a complete column.
this.selectColumn = function (column, forceSelect) {
selectedColumns[column] = selectedColumns[column] &&
!forceSelect ? undefined : true;
THAT.trigger('columnSelected', {
column: column,
selected: selectedColumns[column]
});
if (!selectedColumns[column]) {
var rows = Object.keys(selectedCells);
for (
var i = column,
len = (THAT.dimensions.rows - 1) * (THAT.dimensions.columns) + i;
i < len;
i += THAT.dimensions.columns
) {
selectedCells[i] = undefined;
}
for (var i = rows.length; i--;) {
selectedCells[rows[i]][column] = undefined;
}
}
};
// Select a complete row.
this.selectRow = function (row, forceSelect) {
selectedRows[row] = selectedRows[row] && !forceSelect ? undefined : true;
THAT.trigger('rowSelected', {
row: row,
selected: selectedRows[row]
});
if (!selectedRows[row]) {
for (
var i = row * THAT.dimensions.columns,
len = i + THAT.dimensions.columns;
i < len;
i++
) {
selectedCells[i] = undefined;
}
}
};
// Helper method to check whether a cell is selected or not.
this.cellSelected = function (column, row) {
return selectedCells[getIndex(column, row)] ||
THAT.columnSelected(column) || THAT.rowSelected(row);
};
// Helper method to check whether a column is selected or not.
this.columnSelected = function (column) {
return selectedColumns[column];
};
// Helper method to check whether a row is selected or not.
this.rowSelected = function (row) {
return selectedRows[row];
};
}).refs({
dimensions: {
columns: 1,
rows: 1
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment