Last active
February 23, 2016 16:59
-
-
Save flekschas/d52c5be411bfe2e66e65 to your computer and use it in GitHub Desktop.
Caleydo Web Tutorial - AngularJS heatmap integration
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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]); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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