<!DOCTYPE html>
<html>
<head>
<link data-require="fontawesome@4.1.0" data-semver="4.2.0" rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" />
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="jquery.cytoscape.js-panzoom.css" />
<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/cytoscape/2.3.7/cytoscape.min.js"></script>
<script src="jquery.cytoscape.js-panzoom.js"></script>
<script src="script.js"></script>
</head>
<body>
<h1>Cytoscape.js Panzoom Widget Leak Demo</h1>
<div>Open Dev Tools and take a baseline memory snapshot.
Then Click Init and Destroy one after the other a few times.
Take another snapshot and compare. Look for Detached DOM Tree nodes and look at the retainers.
</div>
<br />
<br />
<button onclick="app.init()">Init</button>
<button onclick="app.destroy()">Destroy</button>
<br />
<br />
<div id="playground"></div>
</body>
</html>
// Code goes here
var CanvasApp = function () {
var cy;
var playground;
return {
init: function () {
var elem = document.createElement("div");
elem.id = 'cy';
playground = document.getElementById('playground');
playground.appendChild(elem);
cy = $(elem).cytoscape({
style: [
{
selector: 'node',
css: {
'content': 'data(name)'
}
},
{
selector: 'edge',
css: {
'target-arrow-shape': 'triangle'
}
}
],
elements: {
nodes: [
{data: {id: 'j', name: 'Jerry'}},
{data: {id: 'e', name: 'Elaine'}},
{data: {id: 'k', name: 'Kramer'}},
{data: {id: 'g', name: 'George'}}
],
edges: [
{data: {source: 'j', target: 'e'}},
{data: {source: 'j', target: 'k'}},
{data: {source: 'j', target: 'g'}},
{data: {source: 'e', target: 'j'}},
{data: {source: 'e', target: 'k'}},
{data: {source: 'k', target: 'j'}},
{data: {source: 'k', target: 'e'}},
{data: {source: 'k', target: 'g'}},
{data: {source: 'g', target: 'j'}}
]
}
}).cytoscape('get');
$(elem).cytoscapePanzoom();
},
destroy: function () {
cy.destroy();
cy = null;
}
}
};
var app = new CanvasApp();
/* Styles go here */
body {
font-family: helvetica;
font-size: 14px;
}
#cy {
width: 600px;
height: 600px;
border: 1px solid #888;
}
;(function($){
var defaults = {
zoomFactor: 0.05, // zoom factor per zoom tick
zoomDelay: 45, // how many ms between zoom ticks
minZoom: 0.1, // min zoom level
maxZoom: 10, // max zoom level
fitPadding: 50, // padding when fitting
panSpeed: 10, // how many ms in between pan ticks
panDistance: 10, // max pan distance per tick
panDragAreaSize: 75, // the length of the pan drag box in which the vector for panning is calculated (bigger = finer control of pan speed and direction)
panMinPercentSpeed: 0.25, // the slowest speed we can pan by (as a percent of panSpeed)
panInactiveArea: 8, // radius of inactive area in pan drag box
panIndicatorMinOpacity: 0.5, // min opacity of pan indicator (the draggable nib); scales from this to 1.0
autodisableForMobile: true, // disable the panzoom completely for mobile (since we don't really need it with gestures like pinch to zoom)
// icon class names
sliderHandleIcon: 'fa fa-minus',
zoomInIcon: 'fa fa-plus',
zoomOutIcon: 'fa fa-minus',
resetIcon: 'fa fa-expand'
};
$.fn.cytoscapePanzoom = function(params){
var options = $.extend(true, {}, defaults, params);
var fn = params;
var functions = {
destroy: function(){
var $this = $(this);
$this.find(".ui-cytoscape-panzoom").remove();
},
init: function(){
var browserIsMobile = 'ontouchstart' in window;
if( browserIsMobile && options.autodisableForMobile ){
return $(this);
}
return $(this).each(function(){
var $container = $(this);
var $panzoom = $('<div class="ui-cytoscape-panzoom"></div>');
$container.append( $panzoom );
if( options.staticPosition ){
$panzoom.addClass("ui-cytoscape-panzoom-static");
}
// add base html elements
/////////////////////////
var $zoomIn = $('<div class="ui-cytoscape-panzoom-zoom-in ui-cytoscape-panzoom-zoom-button"><span class="icon '+ options.zoomInIcon +'"></span></div>');
$panzoom.append( $zoomIn );
var $zoomOut = $('<div class="ui-cytoscape-panzoom-zoom-out ui-cytoscape-panzoom-zoom-button"><span class="icon ' + options.zoomOutIcon + '"></span></div>');
$panzoom.append( $zoomOut );
var $reset = $('<div class="ui-cytoscape-panzoom-reset ui-cytoscape-panzoom-zoom-button"><span class="icon ' + options.resetIcon + '"></span></div>');
$panzoom.append( $reset );
var $slider = $('<div class="ui-cytoscape-panzoom-slider"></div>');
$panzoom.append( $slider );
$slider.append('<div class="ui-cytoscape-panzoom-slider-background"></div>');
var $sliderHandle = $('<div class="ui-cytoscape-panzoom-slider-handle"><span class="icon ' + options.sliderHandleIcon + '"></span></div>');
$slider.append( $sliderHandle );
var $noZoomTick = $('<div class="ui-cytoscape-panzoom-no-zoom-tick"></div>');
$slider.append( $noZoomTick );
var $panner = $('<div class="ui-cytoscape-panzoom-panner"></div>');
$panzoom.append( $panner );
var $pHandle = $('<div class="ui-cytoscape-panzoom-panner-handle"></div>');
$panner.append( $pHandle );
var $pUp = $('<div class="ui-cytoscape-panzoom-pan-up ui-cytoscape-panzoom-pan-button"></div>');
var $pDown = $('<div class="ui-cytoscape-panzoom-pan-down ui-cytoscape-panzoom-pan-button"></div>');
var $pLeft = $('<div class="ui-cytoscape-panzoom-pan-left ui-cytoscape-panzoom-pan-button"></div>');
var $pRight = $('<div class="ui-cytoscape-panzoom-pan-right ui-cytoscape-panzoom-pan-button"></div>');
$panner.append( $pUp ).append( $pDown ).append( $pLeft ).append( $pRight );
var $pIndicator = $('<div class="ui-cytoscape-panzoom-pan-indicator"></div>');
$panner.append( $pIndicator );
// functions for calculating panning
////////////////////////////////////
function handle2pan(e){
var v = {
x: e.originalEvent.pageX - $panner.offset().left - $panner.width()/2,
y: e.originalEvent.pageY - $panner.offset().top - $panner.height()/2
}
var r = options.panDragAreaSize;
var d = Math.sqrt( v.x*v.x + v.y*v.y );
var percent = Math.min( d/r, 1 );
if( d < options.panInactiveArea ){
return {
x: NaN,
y: NaN
};
}
v = {
x: v.x/d,
y: v.y/d
};
percent = Math.max( options.panMinPercentSpeed, percent );
var vnorm = {
x: -1 * v.x * (percent * options.panDistance),
y: -1 * v.y * (percent * options.panDistance)
};
return vnorm;
}
function donePanning(){
clearInterval(panInterval);
$(window).unbind("mousemove", handler);
$pIndicator.hide();
}
function positionIndicator(pan){
var v = pan;
var d = Math.sqrt( v.x*v.x + v.y*v.y );
var vnorm = {
x: -1 * v.x/d,
y: -1 * v.y/d
};
var w = $panner.width();
var h = $panner.height();
var percent = d/options.panDistance;
var opacity = Math.max( options.panIndicatorMinOpacity, percent );
var color = 255 - Math.round( opacity * 255 );
$pIndicator.show().css({
left: w/2 * vnorm.x + w/2,
top: h/2 * vnorm.y + h/2,
background: "rgb(" + color + ", " + color + ", " + color + ")"
});
}
function calculateZoomCenterPoint(){
var cy = $container.cytoscape("get");
var pan = cy.pan();
var zoom = cy.zoom();
zx = $container.width()/2;
zy = $container.height()/2;
}
var zooming = false;
function startZooming(){
zooming = true;
calculateZoomCenterPoint();
}
function endZooming(){
zooming = false;
}
var zx, zy;
function zoomTo(level){
var cy = $container.cytoscape("get");
if( !zooming ){ // for non-continuous zooming (e.g. click slider at pt)
calculateZoomCenterPoint();
}
cy.zoom({
level: level,
renderedPosition: { x: zx, y: zy }
});
}
var panInterval;
var handler = function(e){
e.stopPropagation(); // don't trigger dragging of panzoom
e.preventDefault(); // don't cause text selection
clearInterval(panInterval);
var pan = handle2pan(e);
if( isNaN(pan.x) || isNaN(pan.y) ){
$pIndicator.hide();
return;
}
positionIndicator(pan);
panInterval = setInterval(function(){
$container.cytoscape("get").panBy(pan);
}, options.panSpeed);
};
$pHandle.bind("mousedown", function(e){
// handle click of icon
handler(e);
// update on mousemove
$(window).bind("mousemove", handler);
});
$pHandle.bind("mouseup", function(){
donePanning();
});
$(window).bind("mouseup blur", function(){
donePanning();
});
// set up slider behaviour
//////////////////////////
$slider.bind('mousedown', function(){
return false; // so we don't pan close to the slider handle
});
var sliderVal;
var sliding = false;
var sliderPadding = 2;
function setSliderFromMouse(evt, handleOffset){
if( handleOffset === undefined ){
handleOffset = 0;
}
var padding = sliderPadding;
var min = 0 + padding;
var max = $slider.height() - $sliderHandle.height() - 2*padding;
var top = evt.pageY - $slider.offset().top - handleOffset;
// constrain to slider bounds
if( top < min ){ top = min }
if( top > max ){ top = max }
var percent = 1 - (top - min) / ( max - min );
// move the handle
$sliderHandle.css('top', top);
var zmin = options.minZoom;
var zmax = options.maxZoom;
// assume (zoom = zmax ^ p) where p ranges on (x, 1) with x negative
var x = Math.log(zmin) / Math.log(zmax);
var p = (1 - x)*percent + x;
// change the zoom level
var z = Math.pow( zmax, p );
// bound the zoom value in case of floating pt rounding error
if( z < zmin ){
z = zmin;
} else if( z > zmax ){
z = zmax;
}
zoomTo( z );
}
var sliderMdownHandler, sliderMmoveHandler;
$sliderHandle.bind('mousedown', sliderMdownHandler = function( mdEvt ){
var handleOffset = mdEvt.target === $sliderHandle[0] ? mdEvt.offsetY : 0;
sliding = true;
startZooming();
$sliderHandle.addClass("active");
var lastMove = 0;
$(window).bind('mousemove', sliderMmoveHandler = function( mmEvt ){
var now = +new Date;
// throttle the zooms every 10 ms so we don't call zoom too often and cause lag
if( now > lastMove + 10 ){
lastMove = now;
} else {
return false;
}
setSliderFromMouse(mmEvt, handleOffset);
return false;
});
// unbind when
$(window).bind('mouseup', function(){
$(window).unbind('mousemove', sliderMmoveHandler);
sliding = false;
$sliderHandle.removeClass("active");
endZooming();
});
return false;
});
$slider.bind('mousedown', function(e){
if( e.target !== $sliderHandle[0] ){
sliderMdownHandler(e);
setSliderFromMouse(e);
}
});
function positionSliderFromZoom(){
var cy = $container.cytoscape("get");
var z = cy.zoom();
var zmin = options.minZoom;
var zmax = options.maxZoom;
// assume (zoom = zmax ^ p) where p ranges on (x, 1) with x negative
var x = Math.log(zmin) / Math.log(zmax);
var p = Math.log(z) / Math.log(zmax);
var percent = 1 - (p - x) / (1 - x); // the 1- bit at the front b/c up is in the -ve y direction
var min = sliderPadding;
var max = $slider.height() - $sliderHandle.height() - 2*sliderPadding;
var top = percent * ( max - min );
// constrain to slider bounds
if( top < min ){ top = min }
if( top > max ){ top = max }
// move the handle
$sliderHandle.css('top', top);
}
positionSliderFromZoom();
var cy = $container.cytoscape("get");
cy.on('zoom', function(){
if( !sliding ){
positionSliderFromZoom();
}
});
// set the position of the zoom=1 tick
(function(){
var z = 1;
var zmin = options.minZoom;
var zmax = options.maxZoom;
// assume (zoom = zmax ^ p) where p ranges on (x, 1) with x negative
var x = Math.log(zmin) / Math.log(zmax);
var p = Math.log(z) / Math.log(zmax);
var percent = 1 - (p - x) / (1 - x); // the 1- bit at the front b/c up is in the -ve y direction
if( percent > 1 || percent < 0 ){
$noZoomTick.hide();
return;
}
var min = sliderPadding;
var max = $slider.height() - $sliderHandle.height() - 2*sliderPadding;
var top = percent * ( max - min );
// constrain to slider bounds
if( top < min ){ top = min }
if( top > max ){ top = max }
$noZoomTick.css('top', top);
})();
// set up zoom in/out buttons
/////////////////////////////
function bindButton($button, factor){
var zoomInterval;
$button.bind("mousedown", function(e){
e.preventDefault();
e.stopPropagation();
if( e.button != 0 ){
return;
}
var cy = $container.cytoscape("get");
startZooming();
zoomInterval = setInterval(function(){
var zoom = cy.zoom();
var lvl = cy.zoom() * factor;
if( lvl < options.minZoom ){
lvl = options.minZoom;
}
if( lvl > options.maxZoom ){
lvl = options.maxZoom;
}
if( (lvl == options.maxZoom && zoom == options.maxZoom) ||
(lvl == options.minZoom && zoom == options.minZoom)
){
return;
}
zoomTo(lvl);
}, options.zoomDelay);
return false;
});
$(window).bind("mouseup blur", function(){
clearInterval(zoomInterval);
endZooming();
});
}
bindButton( $zoomIn, (1 + options.zoomFactor) );
bindButton( $zoomOut, (1 - options.zoomFactor) );
$reset.bind("mousedown", function(e){
if( e.button != 0 ){
return;
}
var cy = $container.cytoscape("get");
if( cy.elements().size() === 0 ){
cy.reset();
} else {
cy.fit( options.fitPadding );
}
return false;
});
});
}
};
if( functions[fn] ){
return functions[fn].apply(this, Array.prototype.slice.call( arguments, 1 ));
} else if( typeof fn == 'object' || !fn ) {
return functions.init.apply( this, arguments );
} else {
$.error("No such function `"+ fn +"` for jquery.cytoscapePanzoom");
}
return $(this);
};
$.fn.cyPanzoom = $.fn.cytoscapePanzoom;
})(jQuery);
.ui-cytoscape-panzoom {
position: absolute;
font-size: 12px;
color: #fff;
font-family: arial, helvetica, sans-serif;
line-height: 1;
color: #666;
font-size: 11px;
z-index: 99999;
}
.ui-cytoscape-panzoom-zoom-button {
cursor: pointer;
padding: 3px;
text-align: center;
position: absolute;
border-radius: 3px;
width: 10px;
height: 10px;
left: 16px;
background: #fff;
border: 1px solid #999;
margin-left: -1px;
margin-top: -1px;
z-index: 1;
}
.ui-cytoscape-panzoom-zoom-button:active,
.ui-cytoscape-panzoom-slider-handle:active,
.ui-cytoscape-panzoom-slider-handle.active {
background: #ddd;
}
.ui-cytoscape-panzoom-pan-button {
position: absolute;
z-index: 1;
height: 16px;
width: 16px;
}
.ui-cytoscape-panzoom-reset {
top: 55px;
}
.ui-cytoscape-panzoom-zoom-in {
top: 80px;
}
.ui-cytoscape-panzoom-zoom-out {
top: 197px;
}
.ui-cytoscape-panzoom-pan-up {
top: 0;
left: 50%;
margin-left: -5px;
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-bottom: 5px solid #666;
}
.ui-cytoscape-panzoom-pan-down {
bottom: 0;
left: 50%;
margin-left: -5px;
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 5px solid #666;
}
.ui-cytoscape-panzoom-pan-left {
top: 50%;
left: 0;
margin-top: -5px;
width: 0;
height: 0;
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
border-right: 5px solid #666;
}
.ui-cytoscape-panzoom-pan-right {
top: 50%;
right: 0;
margin-top: -5px;
width: 0;
height: 0;
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
border-left: 5px solid #666;
}
.ui-cytoscape-panzoom-pan-indicator {
position: absolute;
left: 0;
top: 0;
width: 8px;
height: 8px;
border-radius: 8px;
background: #000;
border-radius: 8px;
margin-left: -5px;
margin-top: -5px;
display: none;
z-index: 999;
opacity: 0.6;
}
.ui-cytoscape-panzoom-slider {
position: absolute;
top: 97px;
left: 17px;
height: 100px;
width: 15px;
}
.ui-cytoscape-panzoom-slider-background {
position: absolute;
top: 0;
width: 2px;
height: 100px;
left: 5px;
background: #fff;
border-left: 1px solid #999;
border-right: 1px solid #999;
}
.ui-cytoscape-panzoom-slider-handle {
position: absolute;
width: 16px;
height: 8px;
background: #fff;
border: 1px solid #999;
border-radius: 2px;
margin-left: -2px;
z-index: 999;
line-height: 8px;
cursor: default;
}
.ui-cytoscape-panzoom-slider-handle .icon {
margin: 0 4px;
line-height: 10px;
}
.ui-cytoscape-panzoom-no-zoom-tick {
position: absolute;
background: #666;
border: 1px solid #fff;
border-radius: 2px;
margin-left: -1px;
width: 8px;
height: 2px;
left: 3px;
z-index: 1;
margin-top: 3px;
}
.ui-cytoscape-panzoom-panner {
position: absolute;
left: 5px;
top: 5px;
height: 40px;
width: 40px;
background: #fff;
border: 1px solid #999;
border-radius: 40px;
margin-left: -1px;
}
.ui-cytoscape-panzoom-panner-handle {
position: absolute;
left: 0;
top: 0;
outline: none;
height: 40px;
width: 40px;
position: absolute;
z-index: 999;
}