Skip to content
This repository has been archived by the owner on Jul 29, 2019. It is now read-only.

Selection box #4242

Open
wants to merge 11 commits into
base: develop
Choose a base branch
from
20 changes: 20 additions & 0 deletions docs/network/interaction.html
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ <h3>Options</h3>
bindToWindow: true
},
multiselect: false,
selectionBox: {
enabled: false,
nodes: true,
edges: false,
strokeStyle: "rgb(0,0,0)",
fillStyle: "rgba(0,0,0,.0625)",
edgeAccuracy: 25
}
navigationButtons: false,
selectable: true,
selectConnectedEdges: true,
Expand Down Expand Up @@ -98,13 +106,25 @@ <h3>Options</h3>
<tr><td>hideNodesOnDrag</td> <td>Boolean</td> <td><code>false</code></td> <td>When true, the nodes are not drawn when dragging the view. This can greatly speed up responsiveness on dragging, improving user experience.</td></tr>
<tr><td>hover</td> <td>Boolean</td> <td><code>false</code></td> <td>When true, the nodes use their hover colors when the mouse moves over them.</td></tr>
<tr><td>hoverConnectedEdges</td> <td>Boolean</td> <td><code>true</code></td> <td>When true, on hovering over a node, it's connecting edges are highlighted.</td></tr>

<tr class='toggle collapsible' onclick="toggleTable('optionTable','keyboard', this);"><td><span parent="keyboard" class="right-caret"></span> keyboard</td> <td>Object or Boolean</td> <td><code>Object</code></td> <td>When true, the keyboard shortcuts are enabled with the default settings. For further customization, you can supply an object.</td></tr>
<tr parent="keyboard" class="hidden"><td class="indent">keyboard.enabled</td> <td>Boolean</td> <td><code>false</code></td> <td>Toggle the usage of the keyboard shortcuts. If this option is not defined, it is set to true if any of the properties in this object are defined.</td></tr>
<tr parent="keyboard" class="hidden"><td class="indent">keyboard.speed.x</td> <td>Number</td> <td><code>1</code></td> <td>The speed at which the view moves in the x direction on pressing a key or pressing a navigation button.</td></tr>
<tr parent="keyboard" class="hidden"><td class="indent">keyboard.speed.y</td> <td>Number</td> <td><code>1</code></td> <td>The speed at which the view moves in the y direction on pressing a key or pressing a navigation button.</td></tr>
<tr parent="keyboard" class="hidden"><td class="indent">keyboard.speed.zoom</td> <td>Number</td> <td><code>0.02</code></td> <td>The speed at which the view zooms in or out pressing a key or pressing a navigation button.</td></tr>
<tr parent="keyboard" class="hidden"><td class="indent">keyboard.bindToWindow</td> <td>Boolean</td> <td><code>true</code></td> <td>When binding the keyboard shortcuts to the window, they will work regardless of which DOM object has the focus. If you have multiple networks on your page, you could set this to false, making sure the keyboard shortcuts only work on the network that has the focus.</td></tr>

<tr><td>multiselect</td> <td>Boolean</td> <td><code>false</code></td> <td>When true, a longheld click (or touch) as well as a control-click will add to the selection.</td></tr>

<tr class='toggle collapsible' onclick="toggleTable('optionTable', 'selectionBox', this);"><td><span parent="selectionBox" class="right-caret"></span> selectionBox</td> <td>Object or Boolean</td> <td><code>Object</code></td> <td>When true, the selectionBox is enabled with the default settings. For further customization, you can supply an object.</td></tr>
<tr parent="selectionBox" class="hidden"><td class="indent">selectionBox.enabled</td><td>Boolean</td> <td><code>false</code></td> <td>Toggle the usage of the selectionBox shortcuts</td></tr>
<tr parent="selectionBox" class="hidden"><td class="indent">selectionBox.nodes</td><td>Boolean</td> <td><code>true</code></td> <td>Whether the selection box will select nodes when the user releases the box.</td></tr>
<tr parent="selectionBox" class="hidden"><td class="indent">selectionBox.edges</td><td>Boolean</td> <td><code>false</code></td> <td>Whether the selection box will select edges when the user releases the box.</td></tr>
<tr parent="selectionBox" class="hidden"><td class="indent">selectionBox.strokeStyle</td><td>String</td> <td><code>rgb(0,0,0)</code></td><td>The style of the selection box's border. Any valid <code>color</code>,<code>gradient</code> or <code>pattern</code> will work (see <a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/strokeStyle">MDN: CanvasRenderingContext2D.strokeStyle</a>), but there is no error checking for this value!</td></tr>
<tr parent="selectionBox" class="hidden"><td class="indent">selectionBox.fillStyle</td><td>String</td> <td><code>rgba(0,0,0,.0625)</code></td><td>The style of the selection box's fill. Any valid <code>color</code>,<code>gradient</code> or <code>pattern</code> will work (see <a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle">MDN: CanvasRenderingContext2D.fillStyle</a>), but there is no error checking for this value!</td></tr>
<tr parent="selectionBox" class="hidden"><td class="indent">selectionBox.lineWidth</td><td>Number (integer) >= 0</td> <td><code>2</code></td><td>Width of the border line for the selection box, <i>roughly</i> in pixels (some aliasing occurs due to canvas scaling). When setting this value, Vis.js rounds it down to the nearest integer (i.e., <code>3.2</code> becomes <code>3</code>), and forces it to <code>0</code> if assigned a negative value. A zero value results in no border being drawn.</td></tr>
<tr parent="selectionBox" class="hidden"><td class="indent">selectionBox.edgeAccuracy</td><td>Number</td> <td><code>25</code></td> <td>"Accuracy" of the edge detection. Edges in Vis.js are Bezier curves, so we plot points along an edge and check if a point is within our selection box. The number of points per edge used during this process is specified with this value.</td></tr>

<tr><td>navigationButtons</td> <td>Boolean</td> <td><code>false</code></td> <td>When true, navigation buttons are drawn on the network canvas. These are HTML buttons and can be completely customized using CSS.</td></tr>
<tr><td>selectable</td> <td>Boolean</td><td><code>true</code></td><td>When true, the nodes and edges can be selected by the user.</td></tr>
<tr><td>selectConnectedEdges</td> <td>Boolean</td><td><code>true</code></td><td>When true, on selecting a node, its connecting edges are highlighted.</td></tr>
Expand Down
98 changes: 98 additions & 0 deletions examples/network/other/selectionBox.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<!doctype html>
<html>
<head>
<title>Network | selectionBox option</title>

<style type="text/css">
body {
font: 10pt sans;
}
#mynetwork {
float:left;
width: 600px;
height: 600px;
margin:5px;
border: 1px solid lightgray;
}
#config {
float:left;
width: 400px;
height: 600px;
}

p {
font-size:16px;
max-width:700px;
}
</style>


<script type="text/javascript" src="../exampleUtil.js"></script>
<script type="text/javascript" src="../../../dist/vis.js"></script>
<link href="../../../dist/vis-network.min.css" rel="stylesheet" type="text/css" />

<script type="text/javascript">
var nodes = null;
var edges = null;
var network = null;

function draw() {
nodes = [];
edges = [];
// randomly create some nodes and edges
var data = getScaleFreeNetwork(25);

// create a network
var container = document.getElementById('mynetwork');

var options = {
interaction: {
selectConnectedEdges: false,
selectionBox: true
},
physics: {
stabilization: false
},
configure: ["interaction"]
};
network = new vis.Network(container, data, options);

network.on("configChange", function() {
// this will immediately fix the height of the configuration
// wrapper to prevent unecessary scrolls in chrome.
// see https://github.com/almende/vis/issues/1568
var div = container.getElementsByClassName('vis-configuration-wrapper')[0];
div.style["height"] = div.getBoundingClientRect().height + "px";
});

}
</script>

</head>

<body onload="draw();">

<p>
This example shows how the selection box works. Press and hold the control key and click with your mouse on an area within the Network.
Drag and release to select nodes and edges within the bounds of the resulting box. Here, you can enable / disable the options (or the entire feature) in the configurator.
To modify the options programmatically simply call <code>Ne</code>
</p>
<p>
Options available:
<ul>
<li>"nodes": Select nodes within the box's bounds</li>
<li>"edges": Select edges within the box's bounds</li>
<li>"strokeStyle": An [RGB|RGBA] string to set the color of the box's border</li>
<li>"fillStyle": An RGBA string to set the color of the box's fill (you probably want this at a low opacity)</li>
<li>"lineWidth": Width of line, <i>roughly</i> in pixels (some aliasing occurs due to canvas scaling).</li>
<li>"edgeAccuracy": The number of points generated on each edge that are used to check if the edge is contaned within the box
The default is 25, which means for every edge, we calculate at most 25 evenly distributed points across each line.
</li>
</ul>
</p>
<br />
<div id="mynetwork"></div>

<p id="selection"></p>
</body>
</html>
6 changes: 4 additions & 2 deletions lib/network/Network.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ var {printStyle} = require('./../shared/Validator');
var {allOptions, configureOptions} = require('./options.js');
var KamadaKawai = require("./modules/KamadaKawai.js").default;

var SelectionBox = require('./modules/SelectionBox').default;

/**
* Create a network visualization, displaying nodes and edges.
Expand Down Expand Up @@ -120,9 +121,10 @@ function Network(container, data, options) {
this.groups = new Groups(); // object with groups
this.canvas = new Canvas(this.body); // DOM handler
this.selectionHandler = new SelectionHandler(this.body, this.canvas); // Selection handler
this.interactionHandler = new InteractionHandler(this.body, this.canvas, this.selectionHandler); // Interaction handler handles all the hammer bindings (that are bound by canvas), key
this.selectionBox = new SelectionBox(this.body, this.selectionHandler);
this.interactionHandler = new InteractionHandler(this.body, this.canvas, this.selectionHandler, this.selectionBox); // Interaction handler handles all the hammer bindings (that are bound by canvas), key
this.view = new View(this.body, this.canvas); // camera handler, does animations and zooms
this.renderer = new CanvasRenderer(this.body, this.canvas); // renderer, starts renderloop, has events that modules can hook into
this.renderer = new CanvasRenderer(this.body, this.canvas, this.selectionBox); // renderer, starts renderloop, has events that modules can hook into
this.physics = new PhysicsEngine(this.body); // physics engine, does all the simulations
this.layoutEngine = new LayoutEngine(this.body); // layout engine for inital layout and hierarchical layout
this.clustering = new ClusterEngine(this.body); // clustering api
Expand Down
37 changes: 35 additions & 2 deletions lib/network/modules/CanvasRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,13 @@ class CanvasRenderer {
/**
* @param {Object} body
* @param {Canvas} canvas
* @param {SelectionBox} selectionBox
*/
constructor(body, canvas) {
constructor(body, canvas, selectionBox) {
_initRequestAnimationFrame();
this.body = body;
this.canvas = canvas;
this.selectionBox = selectionBox;

this.redrawRequested = false;
this.renderTimer = undefined;
Expand Down Expand Up @@ -137,7 +139,7 @@ class CanvasRenderer {
* @returns {function|undefined}
* @private
*/
_requestNextFrame(callback, delay) {
_requestNextFrame(callback, delay) {
// During unit testing, it happens that the mock window object is reset while
// the next frame is still pending. Then, either 'window' is not present, or
// 'requestAnimationFrame()' is not present because it is not defined on the
Expand Down Expand Up @@ -273,6 +275,12 @@ class CanvasRenderer {
this._drawNodes(ctx, hidden);
}


// if it should draw selection box
if (this.selectionBox.isActive()) {
this._drawSelectionBox(ctx);
}

ctx.beginPath();
this.body.emitter.emit("afterDrawing", ctx);
ctx.closePath();
Expand All @@ -286,6 +294,31 @@ class CanvasRenderer {
}
}

/**
* Draws the selectionBox
* @param {CanvasRenderingContext2D} ctx
* @private
*/
_drawSelectionBox(ctx) {
ctx.save();
ctx.beginPath();
{
ctx.lineWidth = this.selectionBox.options.lineWidth / this.body.view.scale;
ctx.strokeStyle = this.selectionBox.options.strokeStyle;
ctx.fillStyle = this.selectionBox.options.fillStyle;

// draw the fill rect first, then the border ON TOP of it
ctx.fillRect(this.selectionBox.x, this.selectionBox.y, this.selectionBox.width, this.selectionBox.height);

// CanvasRenderingContext2d.lineWidth ignores 0 values, so if the user has set the width to 0, just don't draw the border
if (this.selectionBox.options.lineWidth >= 1) {
ctx.rect(this.selectionBox.x, this.selectionBox.y, this.selectionBox.width, this.selectionBox.height);
ctx.stroke();
}
}
ctx.closePath();
ctx.restore();
}

/**
* Redraw all nodes
Expand Down
56 changes: 50 additions & 6 deletions lib/network/modules/InteractionHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ let util = require('../../util');
var NavigationHandler = require('./components/NavigationHandler').default;
var Popup = require('./../../shared/Popup').default;


/**
* Handler for interactions
*/
Expand All @@ -11,12 +10,14 @@ class InteractionHandler {
* @param {Object} body
* @param {Canvas} canvas
* @param {SelectionHandler} selectionHandler
* @param {SelectionBox} selectionBox
*/
constructor(body, canvas, selectionHandler) {
constructor(body, canvas, selectionHandler, selectionBox) {
this.body = body;
this.canvas = canvas;
this.selectionHandler = selectionHandler;
this.navigationHandler = new NavigationHandler(body,canvas);
this.selectionBox = selectionBox;

// bind the events from hammer to functions in this object
this.body.eventListeners.onTap = this.onTap.bind(this);
Expand Down Expand Up @@ -61,6 +62,14 @@ class InteractionHandler {
this.bindEventListeners()
}

/**
* @returns {boolean} if the selectionBox option is enabled
* @private
*/
_selectionBoxOption() {
return this.selectionHandler.options.selectionBox.enabled;
}

/**
* Binds event listeners
*/
Expand All @@ -78,7 +87,19 @@ class InteractionHandler {
setOptions(options) {
if (options !== undefined) {
// extend all but the values in fields
let fields = ['hideEdgesOnDrag','hideNodesOnDrag','keyboard','multiselect','selectable','selectConnectedEdges'];
// (there is an overlap between the options "interaction" subobject, with options there taken by three seperate components:
// CanvasRenderer, InteractionHandler and SelectionHandler
// it might be good at some point to break the options and program logic up
// so there is a more direct mapping between the structure of the options in options.js and Network's program logic
let fields = [
'hideEdgesOnDrag' /* see CanvasRenderer */,
'hideNodesOnDrag' /* see CanvasRenderer */,
'keyboard' /* nested object extended below */,
'multiselect' /* see SelectionHandler */,
'selectable' /* see SelectionHandler */,
'selectConnectedEdges' /* see SelectionHandler */,
'selectionBox' /* see SelectionHandler */
];
util.selectiveNotDeepExtend(fields, this.options, options);

// merge the keyboard options in.
Expand All @@ -95,11 +116,10 @@ class InteractionHandler {
this.navigationHandler.setOptions(this.options);
}


/**
* Get the pointer location from a touch location
* @param {{x: number, y: number}} touch
* @return {{x: number, y: number}} pointer
* @returns {{x: number, y: number}} pointer
* @private
*/
getPointer(touch) {
Expand All @@ -109,17 +129,24 @@ class InteractionHandler {
};
}


/**
* On start of a touch gesture, store the pointer
* note for selectionBox functionality -- this Hammer event consumes the "mousedown" DOM event, so we deal with it here
* @param {Event} event The event
* @private
*/
onTouch(event) {
if (new Date().valueOf() - this.touchTime > 50) {
if (this._selectionBoxOption()) {
if (event.srcEvent.ctrlKey) {
this.selectionBox.activate(event.srcEvent);
}
}

this.drag.pointer = this.getPointer(event.center);
this.drag.pinched = false;
this.pinch.scale = this.body.view.scale;

// to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame)
this.touchTime = new Date().valueOf();
}
Expand Down Expand Up @@ -168,6 +195,7 @@ class InteractionHandler {
}



/**
* handle the release of the screen
*
Expand All @@ -176,6 +204,9 @@ class InteractionHandler {
*/
onRelease(event) {
if (new Date().valueOf() - this.touchTime > 10) {
if (this.selectionBox.isActive()) {
this.selectionBox.release();
}
let pointer = this.getPointer(event.center);
this.selectionHandler._generateClickEvent('release', event, pointer);
// to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame)
Expand Down Expand Up @@ -351,6 +382,19 @@ class InteractionHandler {
return;
}

if (this.selectionBox.isActive()) {
/*console.log(event.srcEvent.offsetX, ",", event.srcEvent.offsetY);
console.log(event.srcEvent);
let p = this.canvas.DOMtoCanvas({
x: event.srcEvent.offsetX,
y: event.srcEvent.offsetY
});*/

this.selectionBox.updateBoundingBox(event.srcEvent)

return;
}

// remove the focus on node if it is focussed on by the focusOnNode
this.body.emitter.emit('unlockNode');

Expand Down
Loading