demo workers and network architecture

This commit is contained in:
Dominik Tomičević 2016-03-13 21:51:04 +01:00
parent a762b328b6
commit a90d67e324
95 changed files with 23312 additions and 317 deletions

8
demo/.jshintrc Normal file
View File

@ -0,0 +1,8 @@
{
"esnext": true,
"browser": true,
"globals": {
"sigma": true,
"console": true
}
}

8
demo/static/cypher/.gitattributes vendored Normal file
View File

@ -0,0 +1,8 @@
*.txt text
*.js text
*.html text
*.md text
*.json text
*.yml text
*.css text
*.svg text

8
demo/static/cypher/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
/node_modules
/npm-debug.log
/test*.html
.tern-*
*~
*.swp
.idea
*.iml

View File

@ -0,0 +1,10 @@
/node_modules
/demo
/doc
/test
/test*.html
/index.html
/mode/*/*test.js
/mode/*/*.html
/mode/index.html
.*

View File

@ -0,0 +1,4 @@
language: node_js
node_js:
- stable
sudo: false

View File

@ -0,0 +1,335 @@
/* BASICS */
.CodeMirror {
/* Set height, width, borders, and global font properties here */
font-family: monospace;
height: 300px;
color: black;
}
/* PADDING */
.CodeMirror-lines {
padding: 4px 0; /* Vertical padding around content */
}
.CodeMirror pre {
padding: 0 4px; /* Horizontal padding of content */
}
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
background-color: white; /* The little square between H and V scrollbars */
}
/* GUTTER */
.CodeMirror-gutters {
border-right: 1px solid #ddd;
background-color: #f7f7f7;
white-space: nowrap;
}
.CodeMirror-linenumbers {}
.CodeMirror-linenumber {
padding: 0 3px 0 5px;
min-width: 20px;
text-align: right;
color: #999;
white-space: nowrap;
}
.CodeMirror-guttermarker { color: black; }
.CodeMirror-guttermarker-subtle { color: #999; }
/* CURSOR */
.CodeMirror-cursor {
border-left: 1px solid black;
border-right: none;
width: 0;
}
/* Shown when moving in bi-directional text */
.CodeMirror div.CodeMirror-secondarycursor {
border-left: 1px solid silver;
}
.cm-fat-cursor .CodeMirror-cursor {
width: auto;
border: 0;
background: #7e7;
}
.cm-fat-cursor div.CodeMirror-cursors {
z-index: 1;
}
.cm-animate-fat-cursor {
width: auto;
border: 0;
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
animation: blink 1.06s steps(1) infinite;
background-color: #7e7;
}
@-moz-keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
@-webkit-keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
@keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
/* Can style cursor different in overwrite (non-insert) mode */
.CodeMirror-overwrite .CodeMirror-cursor {}
.cm-tab { display: inline-block; text-decoration: inherit; }
.CodeMirror-ruler {
border-left: 1px solid #ccc;
position: absolute;
}
/* DEFAULT THEME */
.cm-s-default .cm-header {color: blue;}
.cm-s-default .cm-quote {color: #090;}
.cm-negative {color: #d44;}
.cm-positive {color: #292;}
.cm-header, .cm-strong {font-weight: bold;}
.cm-em {font-style: italic;}
.cm-link {text-decoration: underline;}
.cm-strikethrough {text-decoration: line-through;}
.cm-s-default .cm-keyword {color: #708;}
.cm-s-default .cm-atom {color: #219;}
.cm-s-default .cm-number {color: #164;}
.cm-s-default .cm-def {color: #00f;}
.cm-s-default .cm-variable,
.cm-s-default .cm-punctuation,
.cm-s-default .cm-property,
.cm-s-default .cm-operator {}
.cm-s-default .cm-variable-2 {color: #05a;}
.cm-s-default .cm-variable-3 {color: #085;}
.cm-s-default .cm-comment {color: #a50;}
.cm-s-default .cm-string {color: #a11;}
.cm-s-default .cm-string-2 {color: #f50;}
.cm-s-default .cm-meta {color: #555;}
.cm-s-default .cm-qualifier {color: #555;}
.cm-s-default .cm-builtin {color: #30a;}
.cm-s-default .cm-bracket {color: #997;}
.cm-s-default .cm-tag {color: #170;}
.cm-s-default .cm-attribute {color: #00c;}
.cm-s-default .cm-hr {color: #999;}
.cm-s-default .cm-link {color: #00c;}
.cm-s-default .cm-error {color: #f00;}
.cm-invalidchar {color: #f00;}
.CodeMirror-composing { border-bottom: 2px solid; }
/* Default styles for common addons */
div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
.CodeMirror-activeline-background {background: #e8f2ff;}
/* STOP */
/* The rest of this file contains styles related to the mechanics of
the editor. You probably shouldn't touch them. */
.CodeMirror {
position: relative;
overflow: hidden;
background: white;
}
.CodeMirror-scroll {
overflow: scroll !important; /* Things will break if this is overridden */
/* 30px is the magic margin used to hide the element's real scrollbars */
/* See overflow: hidden in .CodeMirror */
margin-bottom: -30px; margin-right: -30px;
padding-bottom: 30px;
height: 100%;
outline: none; /* Prevent dragging from highlighting the element */
position: relative;
}
.CodeMirror-sizer {
position: relative;
border-right: 30px solid transparent;
}
/* The fake, visible scrollbars. Used to force redraw during scrolling
before actual scrolling happens, thus preventing shaking and
flickering artifacts. */
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
position: absolute;
z-index: 6;
display: none;
}
.CodeMirror-vscrollbar {
right: 0; top: 0;
overflow-x: hidden;
overflow-y: scroll;
}
.CodeMirror-hscrollbar {
bottom: 0; left: 0;
overflow-y: hidden;
overflow-x: scroll;
}
.CodeMirror-scrollbar-filler {
right: 0; bottom: 0;
}
.CodeMirror-gutter-filler {
left: 0; bottom: 0;
}
.CodeMirror-gutters {
position: absolute; left: 0; top: 0;
z-index: 3;
}
.CodeMirror-gutter {
white-space: normal;
height: 100%;
display: inline-block;
vertical-align: top;
margin-bottom: -30px;
/* Hack to make IE7 behave */
*zoom:1;
*display:inline;
}
.CodeMirror-gutter-wrapper {
position: absolute;
z-index: 4;
background: none !important;
border: none !important;
}
.CodeMirror-gutter-background {
position: absolute;
top: 0; bottom: 0;
z-index: 4;
}
.CodeMirror-gutter-elt {
position: absolute;
cursor: default;
z-index: 4;
}
.CodeMirror-gutter-wrapper {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.CodeMirror-lines {
cursor: text;
min-height: 1px; /* prevents collapsing before first draw */
}
.CodeMirror pre {
/* Reset some styles that the rest of the page might have set */
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
border-width: 0;
background: transparent;
font-family: inherit;
font-size: inherit;
margin: 0;
white-space: pre;
word-wrap: normal;
line-height: inherit;
color: inherit;
z-index: 2;
position: relative;
overflow: visible;
-webkit-tap-highlight-color: transparent;
}
.CodeMirror-wrap pre {
word-wrap: break-word;
white-space: pre-wrap;
word-break: normal;
}
.CodeMirror-linebackground {
position: absolute;
left: 0; right: 0; top: 0; bottom: 0;
z-index: 0;
}
.CodeMirror-linewidget {
position: relative;
z-index: 2;
overflow: auto;
}
.CodeMirror-widget {}
.CodeMirror-code {
outline: none;
}
/* Force content-box sizing for the elements where we expect it */
.CodeMirror-scroll,
.CodeMirror-sizer,
.CodeMirror-gutter,
.CodeMirror-gutters,
.CodeMirror-linenumber {
-moz-box-sizing: content-box;
box-sizing: content-box;
}
.CodeMirror-measure {
position: absolute;
width: 100%;
height: 0;
overflow: hidden;
visibility: hidden;
}
.CodeMirror-cursor { position: absolute; }
.CodeMirror-measure pre { position: static; }
div.CodeMirror-cursors {
visibility: hidden;
position: relative;
z-index: 3;
}
div.CodeMirror-dragcursors {
visibility: visible;
}
.CodeMirror-focused div.CodeMirror-cursors {
visibility: visible;
}
.CodeMirror-selected { background: #d9d9d9; }
.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
.CodeMirror-crosshair { cursor: crosshair; }
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
.cm-searching {
background: #ffa;
background: rgba(255, 255, 0, .4);
}
/* IE7 hack to prevent it from returning funny offsetTops on the spans */
.CodeMirror span { *vertical-align: text-bottom; }
/* Used to force a border model for a node */
.cm-force-border { padding-right: .1px; }
@media print {
/* Hide the cursor when printing */
.CodeMirror div.CodeMirror-cursors {
visibility: hidden;
}
}
/* See issue #2901 */
.cm-tab-wrap-hack:after { content: ''; }
/* Help users use markselection to safely style text background */
span.CodeMirror-selectedtext { background: none; }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,146 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
// By the Neo4j Team and contributors.
// https://github.com/neo4j-contrib/CodeMirror
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
var wordRegexp = function(words) {
return new RegExp("^(?:" + words.join("|") + ")$", "i");
};
CodeMirror.defineMode("cypher", function(config) {
var tokenBase = function(stream/*, state*/) {
var ch = stream.next();
if (ch === "\"" || ch === "'") {
stream.match(/.+?["']/);
return "string";
}
if (/[{}\(\),\.;\[\]]/.test(ch)) {
curPunc = ch;
return "node";
} else if (ch === "/" && stream.eat("/")) {
stream.skipToEnd();
return "comment";
} else if (operatorChars.test(ch)) {
stream.eatWhile(operatorChars);
return null;
} else {
stream.eatWhile(/[_\w\d]/);
if (stream.eat(":")) {
stream.eatWhile(/[\w\d_\-]/);
return "atom";
}
var word = stream.current();
if (funcs.test(word)) return "builtin";
if (preds.test(word)) return "def";
if (keywords.test(word)) return "keyword";
return "variable";
}
};
var pushContext = function(state, type, col) {
return state.context = {
prev: state.context,
indent: state.indent,
col: col,
type: type
};
};
var popContext = function(state) {
state.indent = state.context.indent;
return state.context = state.context.prev;
};
var indentUnit = config.indentUnit;
var curPunc;
var funcs = wordRegexp(["abs", "acos", "allShortestPaths", "asin", "atan", "atan2", "avg", "ceil", "coalesce", "collect", "cos", "cot", "count", "degrees", "e", "endnode", "exp", "extract", "filter", "floor", "haversin", "head", "id", "keys", "labels", "last", "left", "length", "log", "log10", "lower", "ltrim", "max", "min", "node", "nodes", "percentileCont", "percentileDisc", "pi", "radians", "rand", "range", "reduce", "rel", "relationship", "relationships", "replace", "reverse", "right", "round", "rtrim", "shortestPath", "sign", "sin", "size", "split", "sqrt", "startnode", "stdev", "stdevp", "str", "substring", "sum", "tail", "tan", "timestamp", "toFloat", "toInt", "toString", "trim", "type", "upper"]);
var preds = wordRegexp(["all", "and", "any", "contains", "exists", "has", "in", "none", "not", "or", "single", "xor"]);
var keywords = wordRegexp(["as", "asc", "ascending", "assert", "by", "case", "commit", "constraint", "create", "csv", "cypher", "delete", "desc", "descending", "detach", "distinct", "drop", "else", "end", "ends", "explain", "false", "fieldterminator", "foreach", "from", "headers", "in", "index", "is", "join", "limit", "load", "match", "merge", "null", "on", "optional", "order", "periodic", "profile", "remove", "return", "scan", "set", "skip", "start", "starts", "then", "true", "union", "unique", "unwind", "using", "when", "where", "with"]);
var operatorChars = /[*+\-<>=&|~%^]/;
return {
startState: function(/*base*/) {
return {
tokenize: tokenBase,
context: null,
indent: 0,
col: 0
};
},
token: function(stream, state) {
if (stream.sol()) {
if (state.context && (state.context.align == null)) {
state.context.align = false;
}
state.indent = stream.indentation();
}
if (stream.eatSpace()) {
return null;
}
var style = state.tokenize(stream, state);
if (style !== "comment" && state.context && (state.context.align == null) && state.context.type !== "pattern") {
state.context.align = true;
}
if (curPunc === "(") {
pushContext(state, ")", stream.column());
} else if (curPunc === "[") {
pushContext(state, "]", stream.column());
} else if (curPunc === "{") {
pushContext(state, "}", stream.column());
} else if (/[\]\}\)]/.test(curPunc)) {
while (state.context && state.context.type === "pattern") {
popContext(state);
}
if (state.context && curPunc === state.context.type) {
popContext(state);
}
} else if (curPunc === "." && state.context && state.context.type === "pattern") {
popContext(state);
} else if (/atom|string|variable/.test(style) && state.context) {
if (/[\}\]]/.test(state.context.type)) {
pushContext(state, "pattern", stream.column());
} else if (state.context.type === "pattern" && !state.context.align) {
state.context.align = true;
state.context.col = stream.column();
}
}
return style;
},
indent: function(state, textAfter) {
var firstChar = textAfter && textAfter.charAt(0);
var context = state.context;
if (/[\]\}]/.test(firstChar)) {
while (context && context.type === "pattern") {
context = context.prev;
}
}
var closing = context && firstChar === context.type;
if (!context) return 0;
if (context.type === "keywords") return CodeMirror.commands.newlineAndIndent;
if (context.align) return context.col + (closing ? 0 : 1);
return context.indent + (closing ? 0 : indentUnit);
}
};
});
CodeMirror.modeExtensions["cypher"] = {
autoFormatLineBreaks: function(text) {
var i, lines, reProcessedPortion;
var lines = text.split("\n");
var reProcessedPortion = /\s+\b(return|where|order by|match|with|skip|limit|create|delete|set)\b\s/g;
for (var i = 0; i < lines.length; i++)
lines[i] = lines[i].replace(reProcessedPortion, " \n$1 ").trim();
return lines.join("\n");
}
};
CodeMirror.defineMIME("application/x-cypher-query", "cypher");
});

View File

@ -0,0 +1,43 @@
/* neo theme for codemirror */
/* Color scheme */
.cm-s-neo.CodeMirror {
background-color:#ffffff;
color:#2e383c;
line-height:1.4375;
}
.cm-s-neo .cm-comment { color:#75787b; }
.cm-s-neo .cm-keyword, .cm-s-neo .cm-property { color:#1d75b3; }
.cm-s-neo .cm-atom,.cm-s-neo .cm-number { color:#75438a; }
.cm-s-neo .cm-node,.cm-s-neo .cm-tag { color:#9c3328; }
.cm-s-neo .cm-string { color:#b35e14; }
.cm-s-neo .cm-variable,.cm-s-neo .cm-qualifier { color:#047d65; }
/* Editor styling */
.cm-s-neo pre {
padding:0;
}
.cm-s-neo .CodeMirror-gutters {
border:none;
border-right:10px solid transparent;
background-color:transparent;
}
.cm-s-neo .CodeMirror-linenumber {
padding:0;
color:#e0e2e5;
}
.cm-s-neo .CodeMirror-guttermarker { color: #1d75b3; }
.cm-s-neo .CodeMirror-guttermarker-subtle { color: #e0e2e5; }
.cm-s-neo .CodeMirror-cursor {
width: auto;
border: 0;
background: rgba(155,157,162,0.37);
z-index: 1;
}

17
demo/static/demo.css Normal file
View File

@ -0,0 +1,17 @@
body {
background-color: #eee;
}
.w100 {
width: 100%;
}
.CodeMirror {
border-top: 1px solid #eee;
height: auto;
}
svg, text {
font-size: 28;
}

142
demo/static/demo.html Normal file
View File

@ -0,0 +1,142 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Memgraph Demo</title>
<link rel="stylesheet" href="demo.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.5/css/materialize.min.css">
<link rel="stylesheet" href="cypher/codemirror.css">
<link rel="stylesheet" href="cypher/neo.css">
<link rel="stylesheet" href="https://cdn.rawgit.com/novus/nvd3/v1.8.1/build/nv.d3.css">
<link rel="stylesheet" href="demo.css">
</head>
<body>
<div class="row">
<div class="valign-wrapper">
<h1 class="valign center w100">Memgraph Benchmark</h1>
</div>
</div>
<div class="row">
<div class="col s2">
<div id="q1" class="card">
<div class="qps valign-wrapper">
<div class="col s6">
<h3 id="text" class="valign center w100">25734</h3>
</div>
<div class="col s6">
<canvas id="gauge" width="150" height="60"
class="valign center"></canvas>
</div>
</div>
<textarea id="query">MATCH (n:Person) RETURN n</textarea>
</div>
<div id="q2" class="card">
<div class="qps valign-wrapper">
<div class="col s6">
<h3 id="text" class="valign center w100">25734</h3>
</div>
<div class="col s6">
<canvas id="gauge" width="150" height="60"
class="valign center"></canvas>
</div>
</div>
<textarea id="query">MATCH (n:Person) RETURN n</textarea>
</div>
<div id="q3" class="card">
<div class="qps valign-wrapper">
<div class="col s6">
<h3 id="text" class="valign center w100">25734</h3>
</div>
<div class="col s6">
<canvas id="gauge" width="150" height="60"
class="valign center"></canvas>
</div>
</div>
<textarea id="query">MATCH (n:Person) RETURN n</textarea>
</div>
<div id="q4" class="card">
<div class="qps valign-wrapper">
<div class="col s6">
<h3 id="text" class="valign center w100">25734</h3>
</div>
<div class="col s6">
<canvas id="gauge" width="150" height="60"
class="valign center"></canvas>
</div>
</div>
<textarea id="query">MATCH (n:Person) RETURN n</textarea>
</div>
</div>
<div class="col s8">
<div class="card">
<div class="card-content">
<div id="chart" style="height: 600px;">
<svg></svg>
</div>
</div>
</div>
</div>
<div class="col s2">
<div id="q5" class="card">
<div class="qps valign-wrapper">
<div class="col s6">
<h3 id="text" class="valign center w100">25734</h3>
</div>
<div class="col s6">
<canvas id="gauge" width="150" height="60"
class="valign center"></canvas>
</div>
</div>
<textarea id="query">MATCH (n:Person) RETURN n</textarea>
</div>
<div id="q6" class="card">
<div class="qps valign-wrapper">
<div class="col s6">
<h3 id="text" class="valign center w100">25734</h3>
</div>
<div class="col s6">
<canvas id="gauge" width="150" height="60"
class="valign center"></canvas>
</div>
</div>
<textarea id="query">MATCH (n:Person) RETURN n</textarea>
</div>
<div id="q7" class="card">
<div class="qps valign-wrapper">
<div class="col s6">
<h3 id="text" class="valign center w100">25734</h3>
</div>
<div class="col s6">
<canvas id="gauge" width="150" height="60"
class="valign center"></canvas>
</div>
</div>
<textarea id="query">MATCH (n:Person) RETURN n</textarea>
</div>
<div id="q8" class="card">
<div class="qps valign-wrapper">
<div class="col s6">
<h3 id="text" class="valign center w100">25734</h3>
</div>
<div class="col s6">
<canvas id="gauge" width="150" height="60"
class="valign center"></canvas>
</div>
</div>
<textarea id="query">MATCH (n:Person) RETURN n</textarea>
</div>
</div>
</div>
<script type="text/javascript" src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.5/js/materialize.min.js"></script>
<script type="text/javascript" src="http://bernii.github.io/gauge.js/dist/gauge.min.js"></script>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="https://cdn.rawgit.com/novus/nvd3/v1.8.1/build/nv.d3.min.js"></script>
<script src="cypher/codemirror.js"></script>
<script src="cypher/cypher.js"></script>
<script src="demo.js"></script>
</body>

186
demo/static/demo.js Normal file
View File

@ -0,0 +1,186 @@
(() => {
'use strict';
class QpsGauge {
constructor(canvas) {
this.gauge = new Gauge(canvas).setOptions({
lines: 12, // The number of lines to draw
angle: 0.07, // The length of each line
lineWidth: 0.27, // The line thickness
pointer: {
length: 0.82, // The radius of the inner circle
strokeWidth: 0.073, // The rotation offset
color: '#000' // Fill color
},
// If true, the pointer will not go past the end of the gauge
limitMax: 'true',
strokeColor: '#e0e0e0', // to see which ones work best for you
generateGradient: true,
percentColors: [[0.0, '#d32f2f'], [0.5, '#ffee58'], [1.0, '#388e3c']],
});
this.gauge.animationSpeed = 1;
}
set(value) {
this.gauge.set(value);
}
setMax(value) {
this.gauge.maxValue = this.maxValue = value;
}
getMax() {
return this.maxValue;
}
}
class CypherQuery {
constructor(textarea) {
this.textarea = textarea;
this.editor = CodeMirror.fromTextArea(this.textarea, {
height: 0,
lineNumbers: true,
mode: 'application/x-cypher-query',
indentWithTabs: false,
smartIndent: true,
matchBrackets: true,
theme: 'neo',
viewportMargin: Infinity
});
}
set(value) {
this.editor.getDoc().setValue(value);
}
get(separator) {
separator = separator ? separator : ' ';
this.editor.getDoc().getValue(separator);
}
}
class QpsText {
constructor(element) {
this.element = element;
}
get() {
$(this.element).text();
}
set(text) {
$(this.element).text(text);
}
}
class QueryCard {
constructor(card, maxQps, value) {
this.card = card;
this.maxQps = maxQps;
value = value ? value : 1;
this.text = new QpsText($(card).find('#text')[0]);
this.gauge = new QpsGauge($(card).find('#gauge')[0]);
this.query = new CypherQuery($(card).find('#query')[0]);
this.gauge.setMax(maxQps + 1);
this.gauge.animationSpeed = 1;
this.gauge.set(value);
}
set(value) {
this.text.set(value);
this.gauge.set(value);
}
}
let value = 0;
let maxQps = 5000;
let card1 = new QueryCard($('#q1')[0], maxQps);
let card2 = new QueryCard($('#q2')[0], maxQps);
let card3 = new QueryCard($('#q3')[0], maxQps);
let card4 = new QueryCard($('#q4')[0], maxQps);
let card5 = new QueryCard($('#q5')[0], maxQps);
let card6 = new QueryCard($('#q6')[0], maxQps);
let card7 = new QueryCard($('#q7')[0], maxQps);
let card8 = new QueryCard($('#q8')[0], maxQps);
function run() {
setTimeout(() => {
value += 10;
if(value >= maxQps)
value = 0;
card1.set(value);
card2.set(value);
card3.set(value);
card4.set(value);
card5.set(value);
card6.set(value);
card7.set(value);
card8.set(value);
run();
}, 20);
}
run();
nv.addGraph(function() {
var chart = nv.models.lineChart()
.useInteractiveGuideline(true)
.showLegend(true)
.showYAxis(true)
.showXAxis(true);
chart.xAxis
.axisLabel('Time (s)')
.tickFormat(d3.format(',r'));
chart.yAxis
.axisLabel('QPS')
.tickFormat(d3.format(',r'));
var myData = sinAndCos();
d3.select('#chart svg')
.datum(myData)
.call(chart);
chart.update();
nv.utils.windowResize(function() { chart.update(); });
return chart;
});
function sinAndCos() {
var sin = [],sin2 = [],
cos = [];
for (var i = 0; i < 100; i++) {
sin.push({x: i, y: Math.sin(i/10)});
sin2.push({x: i, y: Math.sin(i/10) *0.25 + 0.5});
cos.push({x: i, y: 0.5 * Math.cos(i/10)});
}
return [{
values: sin,
key: 'Sine Wave',
color: '#ff7f0e'
}, {
values: cos,
key: 'Cosine Wave',
color: '#2ca02c'
}, {
values: sin2,
key: 'Another sine wave',
color: '#7777ff',
area: true
}];
}
})();

6
demo/static/demo.py Normal file
View File

@ -0,0 +1,6 @@
class Chromosome(object):
def __init__(self, k):
pass

View File

@ -0,0 +1,395 @@
;(function(undefined) {
'use strict';
if (typeof sigma === 'undefined')
throw 'sigma is not declared';
// Initialize packages:
sigma.utils.pkg('sigma.captors');
/**
* The user inputs default captor. It deals with mouse events, keyboards
* events and touch events.
*
* @param {DOMElement} target The DOM element where the listeners will be
* bound.
* @param {camera} camera The camera related to the target.
* @param {configurable} settings The settings function.
* @return {sigma.captor} The fresh new captor instance.
*/
sigma.captors.mouse = function(target, camera, settings) {
var _self = this,
_target = target,
_camera = camera,
_settings = settings,
// CAMERA MANAGEMENT:
// ******************
// The camera position when the user starts dragging:
_startCameraX,
_startCameraY,
_startCameraAngle,
// The latest stage position:
_lastCameraX,
_lastCameraY,
_lastCameraAngle,
_lastCameraRatio,
// MOUSE MANAGEMENT:
// *****************
// The mouse position when the user starts dragging:
_startMouseX,
_startMouseY,
_isMouseDown,
_isMoving,
_hasDragged,
_downStartTime,
_movingTimeoutId;
sigma.classes.dispatcher.extend(this);
sigma.utils.doubleClick(_target, 'click', _doubleClickHandler);
_target.addEventListener('DOMMouseScroll', _wheelHandler, false);
_target.addEventListener('mousewheel', _wheelHandler, false);
_target.addEventListener('mousemove', _moveHandler, false);
_target.addEventListener('mousedown', _downHandler, false);
_target.addEventListener('click', _clickHandler, false);
_target.addEventListener('mouseout', _outHandler, false);
document.addEventListener('mouseup', _upHandler, false);
/**
* This method unbinds every handlers that makes the captor work.
*/
this.kill = function() {
sigma.utils.unbindDoubleClick(_target, 'click');
_target.removeEventListener('DOMMouseScroll', _wheelHandler);
_target.removeEventListener('mousewheel', _wheelHandler);
_target.removeEventListener('mousemove', _moveHandler);
_target.removeEventListener('mousedown', _downHandler);
_target.removeEventListener('click', _clickHandler);
_target.removeEventListener('mouseout', _outHandler);
document.removeEventListener('mouseup', _upHandler);
};
// MOUSE EVENTS:
// *************
/**
* The handler listening to the 'move' mouse event. It will effectively
* drag the graph.
*
* @param {event} e A mouse event.
*/
function _moveHandler(e) {
var x,
y,
pos;
// Dispatch event:
if (_settings('mouseEnabled'))
_self.dispatchEvent('mousemove', {
x: sigma.utils.getX(e) - sigma.utils.getWidth(e) / 2,
y: sigma.utils.getY(e) - sigma.utils.getHeight(e) / 2,
clientX: e.clientX,
clientY: e.clientY,
ctrlKey: e.ctrlKey,
metaKey: e.metaKey,
altKey: e.altKey,
shiftKey: e.shiftKey
});
if (_settings('mouseEnabled') && _isMouseDown) {
_isMoving = true;
_hasDragged = true;
if (_movingTimeoutId)
clearTimeout(_movingTimeoutId);
_movingTimeoutId = setTimeout(function() {
_isMoving = false;
}, _settings('dragTimeout'));
sigma.misc.animation.killAll(_camera);
_camera.isMoving = true;
pos = _camera.cameraPosition(
sigma.utils.getX(e) - _startMouseX,
sigma.utils.getY(e) - _startMouseY,
true
);
x = _startCameraX - pos.x;
y = _startCameraY - pos.y;
if (x !== _camera.x || y !== _camera.y) {
_lastCameraX = _camera.x;
_lastCameraY = _camera.y;
_camera.goTo({
x: x,
y: y
});
}
if (e.preventDefault)
e.preventDefault();
else
e.returnValue = false;
e.stopPropagation();
return false;
}
}
/**
* The handler listening to the 'up' mouse event. It will stop dragging the
* graph.
*
* @param {event} e A mouse event.
*/
function _upHandler(e) {
if (_settings('mouseEnabled') && _isMouseDown) {
_isMouseDown = false;
if (_movingTimeoutId)
clearTimeout(_movingTimeoutId);
_camera.isMoving = false;
var x = sigma.utils.getX(e),
y = sigma.utils.getY(e);
if (_isMoving) {
sigma.misc.animation.killAll(_camera);
sigma.misc.animation.camera(
_camera,
{
x: _camera.x +
_settings('mouseInertiaRatio') * (_camera.x - _lastCameraX),
y: _camera.y +
_settings('mouseInertiaRatio') * (_camera.y - _lastCameraY)
},
{
easing: 'quadraticOut',
duration: _settings('mouseInertiaDuration')
}
);
} else if (
_startMouseX !== x ||
_startMouseY !== y
)
_camera.goTo({
x: _camera.x,
y: _camera.y
});
_self.dispatchEvent('mouseup', {
x: x - sigma.utils.getWidth(e) / 2,
y: y - sigma.utils.getHeight(e) / 2,
clientX: e.clientX,
clientY: e.clientY,
ctrlKey: e.ctrlKey,
metaKey: e.metaKey,
altKey: e.altKey,
shiftKey: e.shiftKey
});
// Update _isMoving flag:
_isMoving = false;
}
}
/**
* The handler listening to the 'down' mouse event. It will start observing
* the mouse position for dragging the graph.
*
* @param {event} e A mouse event.
*/
function _downHandler(e) {
if (_settings('mouseEnabled')) {
_startCameraX = _camera.x;
_startCameraY = _camera.y;
_lastCameraX = _camera.x;
_lastCameraY = _camera.y;
_startMouseX = sigma.utils.getX(e);
_startMouseY = sigma.utils.getY(e);
_hasDragged = false;
_downStartTime = (new Date()).getTime();
switch (e.which) {
case 2:
// Middle mouse button pressed
// Do nothing.
break;
case 3:
// Right mouse button pressed
_self.dispatchEvent('rightclick', {
x: _startMouseX - sigma.utils.getWidth(e) / 2,
y: _startMouseY - sigma.utils.getHeight(e) / 2,
clientX: e.clientX,
clientY: e.clientY,
ctrlKey: e.ctrlKey,
metaKey: e.metaKey,
altKey: e.altKey,
shiftKey: e.shiftKey
});
break;
// case 1:
default:
// Left mouse button pressed
_isMouseDown = true;
_self.dispatchEvent('mousedown', {
x: _startMouseX - sigma.utils.getWidth(e) / 2,
y: _startMouseY - sigma.utils.getHeight(e) / 2,
clientX: e.clientX,
clientY: e.clientY,
ctrlKey: e.ctrlKey,
metaKey: e.metaKey,
altKey: e.altKey,
shiftKey: e.shiftKey
});
}
}
}
/**
* The handler listening to the 'out' mouse event. It will just redispatch
* the event.
*
* @param {event} e A mouse event.
*/
function _outHandler(e) {
if (_settings('mouseEnabled'))
_self.dispatchEvent('mouseout');
}
/**
* The handler listening to the 'click' mouse event. It will redispatch the
* click event, but with normalized X and Y coordinates.
*
* @param {event} e A mouse event.
*/
function _clickHandler(e) {
if (_settings('mouseEnabled'))
_self.dispatchEvent('click', {
x: sigma.utils.getX(e) - sigma.utils.getWidth(e) / 2,
y: sigma.utils.getY(e) - sigma.utils.getHeight(e) / 2,
clientX: e.clientX,
clientY: e.clientY,
ctrlKey: e.ctrlKey,
metaKey: e.metaKey,
altKey: e.altKey,
shiftKey: e.shiftKey,
isDragging:
(((new Date()).getTime() - _downStartTime) > 100) &&
_hasDragged
});
if (e.preventDefault)
e.preventDefault();
else
e.returnValue = false;
e.stopPropagation();
return false;
}
/**
* The handler listening to the double click custom event. It will
* basically zoom into the graph.
*
* @param {event} e A mouse event.
*/
function _doubleClickHandler(e) {
var pos,
ratio,
animation;
if (_settings('mouseEnabled')) {
ratio = 1 / _settings('doubleClickZoomingRatio');
_self.dispatchEvent('doubleclick', {
x: _startMouseX - sigma.utils.getWidth(e) / 2,
y: _startMouseY - sigma.utils.getHeight(e) / 2,
clientX: e.clientX,
clientY: e.clientY,
ctrlKey: e.ctrlKey,
metaKey: e.metaKey,
altKey: e.altKey,
shiftKey: e.shiftKey
});
if (_settings('doubleClickEnabled')) {
pos = _camera.cameraPosition(
sigma.utils.getX(e) - sigma.utils.getWidth(e) / 2,
sigma.utils.getY(e) - sigma.utils.getHeight(e) / 2,
true
);
animation = {
duration: _settings('doubleClickZoomDuration')
};
sigma.utils.zoomTo(_camera, pos.x, pos.y, ratio, animation);
}
if (e.preventDefault)
e.preventDefault();
else
e.returnValue = false;
e.stopPropagation();
return false;
}
}
/**
* The handler listening to the 'wheel' mouse event. It will basically zoom
* in or not into the graph.
*
* @param {event} e A mouse event.
*/
function _wheelHandler(e) {
var pos,
ratio,
animation;
if (_settings('mouseEnabled') && _settings('mouseWheelEnabled')) {
ratio = sigma.utils.getDelta(e) > 0 ?
1 / _settings('zoomingRatio') :
_settings('zoomingRatio');
pos = _camera.cameraPosition(
sigma.utils.getX(e) - sigma.utils.getWidth(e) / 2,
sigma.utils.getY(e) - sigma.utils.getHeight(e) / 2,
true
);
animation = {
duration: _settings('mouseZoomDuration')
};
sigma.utils.zoomTo(_camera, pos.x, pos.y, ratio, animation);
if (e.preventDefault)
e.preventDefault();
else
e.returnValue = false;
e.stopPropagation();
return false;
}
}
};
}).call(this);

View File

@ -0,0 +1,430 @@
;(function(undefined) {
'use strict';
if (typeof sigma === 'undefined')
throw 'sigma is not declared';
// Initialize packages:
sigma.utils.pkg('sigma.captors');
/**
* The user inputs default captor. It deals with mouse events, keyboards
* events and touch events.
*
* @param {DOMElement} target The DOM element where the listeners will be
* bound.
* @param {camera} camera The camera related to the target.
* @param {configurable} settings The settings function.
* @return {sigma.captor} The fresh new captor instance.
*/
sigma.captors.touch = function(target, camera, settings) {
var _self = this,
_target = target,
_camera = camera,
_settings = settings,
// CAMERA MANAGEMENT:
// ******************
// The camera position when the user starts dragging:
_startCameraX,
_startCameraY,
_startCameraAngle,
_startCameraRatio,
// The latest stage position:
_lastCameraX,
_lastCameraY,
_lastCameraAngle,
_lastCameraRatio,
// TOUCH MANAGEMENT:
// *****************
// Touches that are down:
_downTouches = [],
_startTouchX0,
_startTouchY0,
_startTouchX1,
_startTouchY1,
_startTouchAngle,
_startTouchDistance,
_touchMode,
_isMoving,
_doubleTap,
_movingTimeoutId;
sigma.classes.dispatcher.extend(this);
sigma.utils.doubleClick(_target, 'touchstart', _doubleTapHandler);
_target.addEventListener('touchstart', _handleStart, false);
_target.addEventListener('touchend', _handleLeave, false);
_target.addEventListener('touchcancel', _handleLeave, false);
_target.addEventListener('touchleave', _handleLeave, false);
_target.addEventListener('touchmove', _handleMove, false);
function position(e) {
var offset = sigma.utils.getOffset(_target);
return {
x: e.pageX - offset.left,
y: e.pageY - offset.top
};
}
/**
* This method unbinds every handlers that makes the captor work.
*/
this.kill = function() {
sigma.utils.unbindDoubleClick(_target, 'touchstart');
_target.addEventListener('touchstart', _handleStart);
_target.addEventListener('touchend', _handleLeave);
_target.addEventListener('touchcancel', _handleLeave);
_target.addEventListener('touchleave', _handleLeave);
_target.addEventListener('touchmove', _handleMove);
};
// TOUCH EVENTS:
// *************
/**
* The handler listening to the 'touchstart' event. It will set the touch
* mode ("_touchMode") and start observing the user touch moves.
*
* @param {event} e A touch event.
*/
function _handleStart(e) {
if (_settings('touchEnabled')) {
var x0,
x1,
y0,
y1,
pos0,
pos1;
_downTouches = e.touches;
switch (_downTouches.length) {
case 1:
_camera.isMoving = true;
_touchMode = 1;
_startCameraX = _camera.x;
_startCameraY = _camera.y;
_lastCameraX = _camera.x;
_lastCameraY = _camera.y;
pos0 = position(_downTouches[0]);
_startTouchX0 = pos0.x;
_startTouchY0 = pos0.y;
break;
case 2:
_camera.isMoving = true;
_touchMode = 2;
pos0 = position(_downTouches[0]);
pos1 = position(_downTouches[1]);
x0 = pos0.x;
y0 = pos0.y;
x1 = pos1.x;
y1 = pos1.y;
_lastCameraX = _camera.x;
_lastCameraY = _camera.y;
_startCameraAngle = _camera.angle;
_startCameraRatio = _camera.ratio;
_startCameraX = _camera.x;
_startCameraY = _camera.y;
_startTouchX0 = x0;
_startTouchY0 = y0;
_startTouchX1 = x1;
_startTouchY1 = y1;
_startTouchAngle = Math.atan2(
_startTouchY1 - _startTouchY0,
_startTouchX1 - _startTouchX0
);
_startTouchDistance = Math.sqrt(
Math.pow(_startTouchY1 - _startTouchY0, 2) +
Math.pow(_startTouchX1 - _startTouchX0, 2)
);
e.preventDefault();
return false;
}
}
}
/**
* The handler listening to the 'touchend', 'touchcancel' and 'touchleave'
* event. It will update the touch mode if there are still at least one
* finger, and stop dragging else.
*
* @param {event} e A touch event.
*/
function _handleLeave(e) {
if (_settings('touchEnabled')) {
_downTouches = e.touches;
var inertiaRatio = _settings('touchInertiaRatio');
if (_movingTimeoutId) {
_isMoving = false;
clearTimeout(_movingTimeoutId);
}
switch (_touchMode) {
case 2:
if (e.touches.length === 1) {
_handleStart(e);
e.preventDefault();
break;
}
/* falls through */
case 1:
_camera.isMoving = false;
_self.dispatchEvent('stopDrag');
if (_isMoving) {
_doubleTap = false;
sigma.misc.animation.camera(
_camera,
{
x: _camera.x +
inertiaRatio * (_camera.x - _lastCameraX),
y: _camera.y +
inertiaRatio * (_camera.y - _lastCameraY)
},
{
easing: 'quadraticOut',
duration: _settings('touchInertiaDuration')
}
);
}
_isMoving = false;
_touchMode = 0;
break;
}
}
}
/**
* The handler listening to the 'touchmove' event. It will effectively drag
* the graph, and eventually zooms and turn it if the user is using two
* fingers.
*
* @param {event} e A touch event.
*/
function _handleMove(e) {
if (!_doubleTap && _settings('touchEnabled')) {
var x0,
x1,
y0,
y1,
cos,
sin,
end,
pos0,
pos1,
diff,
start,
dAngle,
dRatio,
newStageX,
newStageY,
newStageRatio,
newStageAngle;
_downTouches = e.touches;
_isMoving = true;
if (_movingTimeoutId)
clearTimeout(_movingTimeoutId);
_movingTimeoutId = setTimeout(function() {
_isMoving = false;
}, _settings('dragTimeout'));
switch (_touchMode) {
case 1:
pos0 = position(_downTouches[0]);
x0 = pos0.x;
y0 = pos0.y;
diff = _camera.cameraPosition(
x0 - _startTouchX0,
y0 - _startTouchY0,
true
);
newStageX = _startCameraX - diff.x;
newStageY = _startCameraY - diff.y;
if (newStageX !== _camera.x || newStageY !== _camera.y) {
_lastCameraX = _camera.x;
_lastCameraY = _camera.y;
_camera.goTo({
x: newStageX,
y: newStageY
});
_self.dispatchEvent('mousemove', {
x: pos0.x - sigma.utils.getWidth(e) / 2,
y: pos0.y - sigma.utils.getHeight(e) / 2,
clientX: e.clientX,
clientY: e.clientY,
ctrlKey: e.ctrlKey,
metaKey: e.metaKey,
altKey: e.altKey,
shiftKey: e.shiftKey
});
_self.dispatchEvent('drag');
}
break;
case 2:
pos0 = position(_downTouches[0]);
pos1 = position(_downTouches[1]);
x0 = pos0.x;
y0 = pos0.y;
x1 = pos1.x;
y1 = pos1.y;
start = _camera.cameraPosition(
(_startTouchX0 + _startTouchX1) / 2 -
sigma.utils.getWidth(e) / 2,
(_startTouchY0 + _startTouchY1) / 2 -
sigma.utils.getHeight(e) / 2,
true
);
end = _camera.cameraPosition(
(x0 + x1) / 2 - sigma.utils.getWidth(e) / 2,
(y0 + y1) / 2 - sigma.utils.getHeight(e) / 2,
true
);
dAngle = Math.atan2(y1 - y0, x1 - x0) - _startTouchAngle;
dRatio = Math.sqrt(
Math.pow(y1 - y0, 2) + Math.pow(x1 - x0, 2)
) / _startTouchDistance;
// Translation:
x0 = start.x;
y0 = start.y;
// Homothetic transformation:
newStageRatio = _startCameraRatio / dRatio;
x0 = x0 * dRatio;
y0 = y0 * dRatio;
// Rotation:
newStageAngle = _startCameraAngle - dAngle;
cos = Math.cos(-dAngle);
sin = Math.sin(-dAngle);
x1 = x0 * cos + y0 * sin;
y1 = y0 * cos - x0 * sin;
x0 = x1;
y0 = y1;
// Finalize:
newStageX = x0 - end.x + _startCameraX;
newStageY = y0 - end.y + _startCameraY;
if (
newStageRatio !== _camera.ratio ||
newStageAngle !== _camera.angle ||
newStageX !== _camera.x ||
newStageY !== _camera.y
) {
_lastCameraX = _camera.x;
_lastCameraY = _camera.y;
_lastCameraAngle = _camera.angle;
_lastCameraRatio = _camera.ratio;
_camera.goTo({
x: newStageX,
y: newStageY,
angle: newStageAngle,
ratio: newStageRatio
});
_self.dispatchEvent('drag');
}
break;
}
e.preventDefault();
return false;
}
}
/**
* The handler listening to the double tap custom event. It will
* basically zoom into the graph.
*
* @param {event} e A touch event.
*/
function _doubleTapHandler(e) {
var pos,
ratio,
animation;
if (e.touches && e.touches.length === 1 && _settings('touchEnabled')) {
_doubleTap = true;
ratio = 1 / _settings('doubleClickZoomingRatio');
pos = position(e.touches[0]);
_self.dispatchEvent('doubleclick', {
x: pos.x - sigma.utils.getWidth(e) / 2,
y: pos.y - sigma.utils.getHeight(e) / 2,
clientX: e.clientX,
clientY: e.clientY,
ctrlKey: e.ctrlKey,
metaKey: e.metaKey,
altKey: e.altKey,
shiftKey: e.shiftKey
});
if (_settings('doubleClickEnabled')) {
pos = _camera.cameraPosition(
pos.x - sigma.utils.getWidth(e) / 2,
pos.y - sigma.utils.getHeight(e) / 2,
true
);
animation = {
duration: _settings('doubleClickZoomDuration'),
onComplete: function() {
_doubleTap = false;
}
};
sigma.utils.zoomTo(_camera, pos.x, pos.y, ratio, animation);
}
if (e.preventDefault)
e.preventDefault();
else
e.returnValue = false;
e.stopPropagation();
return false;
}
}
};
}).call(this);

View File

@ -0,0 +1,240 @@
;(function(undefined) {
'use strict';
if (typeof sigma === 'undefined')
throw 'sigma is not declared';
sigma.utils.pkg('sigma.classes');
/**
* The camera constructor. It just initializes its attributes and methods.
*
* @param {string} id The id.
* @param {sigma.classes.graph} graph The graph.
* @param {configurable} settings The settings function.
* @param {?object} options Eventually some overriding options.
* @return {camera} Returns the fresh new camera instance.
*/
sigma.classes.camera = function(id, graph, settings, options) {
sigma.classes.dispatcher.extend(this);
Object.defineProperty(this, 'graph', {
value: graph
});
Object.defineProperty(this, 'id', {
value: id
});
Object.defineProperty(this, 'readPrefix', {
value: 'read_cam' + id + ':'
});
Object.defineProperty(this, 'prefix', {
value: 'cam' + id + ':'
});
this.x = 0;
this.y = 0;
this.ratio = 1;
this.angle = 0;
this.isAnimated = false;
this.settings = (typeof options === 'object' && options) ?
settings.embedObject(options) :
settings;
};
/**
* Updates the camera position.
*
* @param {object} coordinates The new coordinates object.
* @return {camera} Returns the camera.
*/
sigma.classes.camera.prototype.goTo = function(coordinates) {
if (!this.settings('enableCamera'))
return this;
var i,
l,
c = coordinates || {},
keys = ['x', 'y', 'ratio', 'angle'];
for (i = 0, l = keys.length; i < l; i++)
if (c[keys[i]] !== undefined) {
if (typeof c[keys[i]] === 'number' && !isNaN(c[keys[i]]))
this[keys[i]] = c[keys[i]];
else
throw 'Value for "' + keys[i] + '" is not a number.';
}
this.dispatchEvent('coordinatesUpdated');
return this;
};
/**
* This method takes a graph and computes for each node and edges its
* coordinates relatively to the center of the camera. Basically, it will
* compute the coordinates that will be used by the graphic renderers.
*
* Since it should be possible to use different cameras and different
* renderers, it is possible to specify a prefix to put before the new
* coordinates (to get something like "node.camera1_x")
*
* @param {?string} read The prefix of the coordinates to read.
* @param {?string} write The prefix of the coordinates to write.
* @param {?object} options Eventually an object of options. Those can be:
* - A restricted nodes array.
* - A restricted edges array.
* - A width.
* - A height.
* @return {camera} Returns the camera.
*/
sigma.classes.camera.prototype.applyView = function(read, write, options) {
options = options || {};
write = write !== undefined ? write : this.prefix;
read = read !== undefined ? read : this.readPrefix;
var nodes = options.nodes || this.graph.nodes(),
edges = options.edges || this.graph.edges();
var i,
l,
node,
cos = Math.cos(this.angle),
sin = Math.sin(this.angle),
nodeRatio = Math.pow(this.ratio, this.settings('nodesPowRatio')),
edgeRatio = Math.pow(this.ratio, this.settings('edgesPowRatio'));
for (i = 0, l = nodes.length; i < l; i++) {
node = nodes[i];
node[write + 'x'] =
(
((node[read + 'x'] || 0) - this.x) * cos +
((node[read + 'y'] || 0) - this.y) * sin
) / this.ratio + (options.width || 0) / 2;
node[write + 'y'] =
(
((node[read + 'y'] || 0) - this.y) * cos -
((node[read + 'x'] || 0) - this.x) * sin
) / this.ratio + (options.height || 0) / 2;
node[write + 'size'] =
(node[read + 'size'] || 0) /
nodeRatio;
}
for (i = 0, l = edges.length; i < l; i++) {
edges[i][write + 'size'] =
(edges[i][read + 'size'] || 0) /
edgeRatio;
}
return this;
};
/**
* This function converts the coordinates of a point from the frame of the
* camera to the frame of the graph.
*
* @param {number} x The X coordinate of the point in the frame of the
* camera.
* @param {number} y The Y coordinate of the point in the frame of the
* camera.
* @return {object} The point coordinates in the frame of the graph.
*/
sigma.classes.camera.prototype.graphPosition = function(x, y, vector) {
var X = 0,
Y = 0,
cos = Math.cos(this.angle),
sin = Math.sin(this.angle);
// Revert the origin differential vector:
if (!vector) {
X = - (this.x * cos + this.y * sin) / this.ratio;
Y = - (this.y * cos - this.x * sin) / this.ratio;
}
return {
x: (x * cos + y * sin) / this.ratio + X,
y: (y * cos - x * sin) / this.ratio + Y
};
};
/**
* This function converts the coordinates of a point from the frame of the
* graph to the frame of the camera.
*
* @param {number} x The X coordinate of the point in the frame of the
* graph.
* @param {number} y The Y coordinate of the point in the frame of the
* graph.
* @return {object} The point coordinates in the frame of the camera.
*/
sigma.classes.camera.prototype.cameraPosition = function(x, y, vector) {
var X = 0,
Y = 0,
cos = Math.cos(this.angle),
sin = Math.sin(this.angle);
// Revert the origin differential vector:
if (!vector) {
X = - (this.x * cos + this.y * sin) / this.ratio;
Y = - (this.y * cos - this.x * sin) / this.ratio;
}
return {
x: ((x - X) * cos - (y - Y) * sin) * this.ratio,
y: ((y - Y) * cos + (x - X) * sin) * this.ratio
};
};
/**
* This method returns the transformation matrix of the camera. This is
* especially useful to apply the camera view directly in shaders, in case of
* WebGL rendering.
*
* @return {array} The transformation matrix.
*/
sigma.classes.camera.prototype.getMatrix = function() {
var scale = sigma.utils.matrices.scale(1 / this.ratio),
rotation = sigma.utils.matrices.rotation(this.angle),
translation = sigma.utils.matrices.translation(-this.x, -this.y),
matrix = sigma.utils.matrices.multiply(
translation,
sigma.utils.matrices.multiply(
rotation,
scale
)
);
return matrix;
};
/**
* Taking a width and a height as parameters, this method returns the
* coordinates of the rectangle representing the camera on screen, in the
* graph's referentiel.
*
* To keep displaying labels of nodes going out of the screen, the method
* keeps a margin around the screen in the returned rectangle.
*
* @param {number} width The width of the screen.
* @param {number} height The height of the screen.
* @return {object} The rectangle as x1, y1, x2 and y2, representing
* two opposite points.
*/
sigma.classes.camera.prototype.getRectangle = function(width, height) {
var widthVect = this.cameraPosition(width, 0, true),
heightVect = this.cameraPosition(0, height, true),
centerVect = this.cameraPosition(width / 2, height / 2, true),
marginX = this.cameraPosition(width / 4, 0, true).x,
marginY = this.cameraPosition(0, height / 4, true).y;
return {
x1: this.x - centerVect.x - marginX,
y1: this.y - centerVect.y - marginY,
x2: this.x - centerVect.x + marginX + widthVect.x,
y2: this.y - centerVect.y - marginY + widthVect.y,
height: Math.sqrt(
Math.pow(heightVect.x, 2) +
Math.pow(heightVect.y + 2 * marginY, 2)
)
};
};
}).call(this);

View File

@ -0,0 +1,116 @@
;(function() {
'use strict';
/**
* This utils aims to facilitate the manipulation of each instance setting.
* Using a function instead of an object brings two main advantages: First,
* it will be easier in the future to catch settings updates through a
* function than an object. Second, giving it a full object will "merge" it
* to the settings object properly, keeping us to have to always add a loop.
*
* @return {configurable} The "settings" function.
*/
var configurable = function() {
var i,
l,
data = {},
datas = Array.prototype.slice.call(arguments, 0);
/**
* The method to use to set or get any property of this instance.
*
* @param {string|object} a1 If it is a string and if a2 is undefined,
* then it will return the corresponding
* property. If it is a string and if a2 is
* set, then it will set a2 as the property
* corresponding to a1, and return this. If
* it is an object, then each pair string +
* object(or any other type) will be set as a
* property.
* @param {*?} a2 The new property corresponding to a1 if a1
* is a string.
* @return {*|configurable} Returns itself or the corresponding
* property.
*
* Polymorphism:
* *************
* Here are some basic use examples:
*
* > settings = new configurable();
* > settings('mySetting', 42);
* > settings('mySetting'); // Logs: 42
* > settings('mySetting', 123);
* > settings('mySetting'); // Logs: 123
* > settings({mySetting: 456});
* > settings('mySetting'); // Logs: 456
*
* Also, it is possible to use the function as a fallback:
* > settings({mySetting: 'abc'}, 'mySetting'); // Logs: 'abc'
* > settings({hisSetting: 'abc'}, 'mySetting'); // Logs: 456
*/
var settings = function(a1, a2) {
var o,
i,
l,
k;
if (arguments.length === 1 && typeof a1 === 'string') {
if (data[a1] !== undefined)
return data[a1];
for (i = 0, l = datas.length; i < l; i++)
if (datas[i][a1] !== undefined)
return datas[i][a1];
return undefined;
} else if (typeof a1 === 'object' && typeof a2 === 'string') {
return (a1 || {})[a2] !== undefined ? a1[a2] : settings(a2);
} else {
o = (typeof a1 === 'object' && a2 === undefined) ? a1 : {};
if (typeof a1 === 'string')
o[a1] = a2;
for (i = 0, k = Object.keys(o), l = k.length; i < l; i++)
data[k[i]] = o[k[i]];
return this;
}
};
/**
* This method returns a new configurable function, with new objects
*
* @param {object*} Any number of objects to search in.
* @return {function} Returns the function. Check its documentation to know
* more about how it works.
*/
settings.embedObjects = function() {
var args = datas.concat(
data
).concat(
Array.prototype.splice.call(arguments, 0)
);
return configurable.apply({}, args);
};
// Initialize
for (i = 0, l = arguments.length; i < l; i++)
settings(arguments[i]);
return settings;
};
/**
* EXPORT:
* *******
*/
if (typeof this.sigma !== 'undefined') {
this.sigma.classes = this.sigma.classes || {};
this.sigma.classes.configurable = configurable;
} else if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports)
exports = module.exports = configurable;
exports.configurable = configurable;
} else
this.configurable = configurable;
}).call(this);

View File

@ -0,0 +1,204 @@
;(function() {
'use strict';
/**
* Dispatcher constructor.
*
* @return {dispatcher} The new dispatcher instance.
*/
var dispatcher = function() {
Object.defineProperty(this, '_handlers', {
value: {}
});
};
/**
* Will execute the handler everytime that the indicated event (or the
* indicated events) will be triggered.
*
* @param {string} events The name of the event (or the events
* separated by spaces).
* @param {function(Object)} handler The handler to bind.
* @return {dispatcher} Returns the instance itself.
*/
dispatcher.prototype.bind = function(events, handler) {
var i,
l,
event,
eArray;
if (
arguments.length === 1 &&
typeof arguments[0] === 'object'
)
for (events in arguments[0])
this.bind(events, arguments[0][events]);
else if (
arguments.length === 2 &&
typeof arguments[1] === 'function'
) {
eArray = typeof events === 'string' ? events.split(' ') : events;
for (i = 0, l = eArray.length; i !== l; i += 1) {
event = eArray[i];
// Check that event is not '':
if (!event)
continue;
if (!this._handlers[event])
this._handlers[event] = [];
// Using an object instead of directly the handler will make possible
// later to add flags
this._handlers[event].push({
handler: handler
});
}
} else
throw 'bind: Wrong arguments.';
return this;
};
/**
* Removes the handler from a specified event (or specified events).
*
* @param {?string} events The name of the event (or the events
* separated by spaces). If undefined,
* then all handlers are removed.
* @param {?function(object)} handler The handler to unbind. If undefined,
* each handler bound to the event or the
* events will be removed.
* @return {dispatcher} Returns the instance itself.
*/
dispatcher.prototype.unbind = function(events, handler) {
var i,
n,
j,
m,
k,
a,
event,
eArray = typeof events === 'string' ? events.split(' ') : events;
if (!arguments.length) {
for (k in this._handlers)
delete this._handlers[k];
return this;
}
if (handler) {
for (i = 0, n = eArray.length; i !== n; i += 1) {
event = eArray[i];
if (this._handlers[event]) {
a = [];
for (j = 0, m = this._handlers[event].length; j !== m; j += 1)
if (this._handlers[event][j].handler !== handler)
a.push(this._handlers[event][j]);
this._handlers[event] = a;
}
if (this._handlers[event] && this._handlers[event].length === 0)
delete this._handlers[event];
}
} else
for (i = 0, n = eArray.length; i !== n; i += 1)
delete this._handlers[eArray[i]];
return this;
};
/**
* Executes each handler bound to the event
*
* @param {string} events The name of the event (or the events separated
* by spaces).
* @param {?object} data The content of the event (optional).
* @return {dispatcher} Returns the instance itself.
*/
dispatcher.prototype.dispatchEvent = function(events, data) {
var i,
n,
j,
m,
a,
event,
eventName,
self = this,
eArray = typeof events === 'string' ? events.split(' ') : events;
data = data === undefined ? {} : data;
for (i = 0, n = eArray.length; i !== n; i += 1) {
eventName = eArray[i];
if (this._handlers[eventName]) {
event = self.getEvent(eventName, data);
a = [];
for (j = 0, m = this._handlers[eventName].length; j !== m; j += 1) {
this._handlers[eventName][j].handler(event);
if (!this._handlers[eventName][j].one)
a.push(this._handlers[eventName][j]);
}
this._handlers[eventName] = a;
}
}
return this;
};
/**
* Return an event object.
*
* @param {string} events The name of the event.
* @param {?object} data The content of the event (optional).
* @return {object} Returns the instance itself.
*/
dispatcher.prototype.getEvent = function(event, data) {
return {
type: event,
data: data || {},
target: this
};
};
/**
* A useful function to deal with inheritance. It will make the target
* inherit the prototype of the class dispatcher as well as its constructor.
*
* @param {object} target The target.
*/
dispatcher.extend = function(target, args) {
var k;
for (k in dispatcher.prototype)
if (dispatcher.prototype.hasOwnProperty(k))
target[k] = dispatcher.prototype[k];
dispatcher.apply(target, args);
};
/**
* EXPORT:
* *******
*/
if (typeof this.sigma !== 'undefined') {
this.sigma.classes = this.sigma.classes || {};
this.sigma.classes.dispatcher = dispatcher;
} else if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports)
exports = module.exports = dispatcher;
exports.dispatcher = dispatcher;
} else
this.dispatcher = dispatcher;
}).call(this);

View File

@ -0,0 +1,832 @@
;(function(undefined) {
'use strict';
/**
* Sigma Quadtree Module for edges
* ===============================
*
* Author: Sébastien Heymann,
* from the quad of Guillaume Plique (Yomguithereal)
* Version: 0.2
*/
/**
* Quad Geometric Operations
* -------------------------
*
* A useful batch of geometric operations used by the quadtree.
*/
var _geom = {
/**
* Transforms a graph node with x, y and size into an
* axis-aligned square.
*
* @param {object} A graph node with at least a point (x, y) and a size.
* @return {object} A square: two points (x1, y1), (x2, y2) and height.
*/
pointToSquare: function(n) {
return {
x1: n.x - n.size,
y1: n.y - n.size,
x2: n.x + n.size,
y2: n.y - n.size,
height: n.size * 2
};
},
/**
* Transforms a graph edge with x1, y1, x2, y2 and size into an
* axis-aligned square.
*
* @param {object} A graph edge with at least two points
* (x1, y1), (x2, y2) and a size.
* @return {object} A square: two points (x1, y1), (x2, y2) and height.
*/
lineToSquare: function(e) {
if (e.y1 < e.y2) {
// (e.x1, e.y1) on top
if (e.x1 < e.x2) {
// (e.x1, e.y1) on left
return {
x1: e.x1 - e.size,
y1: e.y1 - e.size,
x2: e.x2 + e.size,
y2: e.y1 - e.size,
height: e.y2 - e.y1 + e.size * 2
};
}
// (e.x1, e.y1) on right
return {
x1: e.x2 - e.size,
y1: e.y1 - e.size,
x2: e.x1 + e.size,
y2: e.y1 - e.size,
height: e.y2 - e.y1 + e.size * 2
};
}
// (e.x2, e.y2) on top
if (e.x1 < e.x2) {
// (e.x1, e.y1) on left
return {
x1: e.x1 - e.size,
y1: e.y2 - e.size,
x2: e.x2 + e.size,
y2: e.y2 - e.size,
height: e.y1 - e.y2 + e.size * 2
};
}
// (e.x2, e.y2) on right
return {
x1: e.x2 - e.size,
y1: e.y2 - e.size,
x2: e.x1 + e.size,
y2: e.y2 - e.size,
height: e.y1 - e.y2 + e.size * 2
};
},
/**
* Transforms a graph edge of type 'curve' with x1, y1, x2, y2,
* control point and size into an axis-aligned square.
*
* @param {object} e A graph edge with at least two points
* (x1, y1), (x2, y2) and a size.
* @param {object} cp A control point (x,y).
* @return {object} A square: two points (x1, y1), (x2, y2) and height.
*/
quadraticCurveToSquare: function(e, cp) {
var pt = sigma.utils.getPointOnQuadraticCurve(
0.5,
e.x1,
e.y1,
e.x2,
e.y2,
cp.x,
cp.y
);
// Bounding box of the two points and the point at the middle of the
// curve:
var minX = Math.min(e.x1, e.x2, pt.x),
maxX = Math.max(e.x1, e.x2, pt.x),
minY = Math.min(e.y1, e.y2, pt.y),
maxY = Math.max(e.y1, e.y2, pt.y);
return {
x1: minX - e.size,
y1: minY - e.size,
x2: maxX + e.size,
y2: minY - e.size,
height: maxY - minY + e.size * 2
};
},
/**
* Transforms a graph self loop into an axis-aligned square.
*
* @param {object} n A graph node with a point (x, y) and a size.
* @return {object} A square: two points (x1, y1), (x2, y2) and height.
*/
selfLoopToSquare: function(n) {
// Fitting to the curve is too costly, we compute a larger bounding box
// using the control points:
var cp = sigma.utils.getSelfLoopControlPoints(n.x, n.y, n.size);
// Bounding box of the point and the two control points:
var minX = Math.min(n.x, cp.x1, cp.x2),
maxX = Math.max(n.x, cp.x1, cp.x2),
minY = Math.min(n.y, cp.y1, cp.y2),
maxY = Math.max(n.y, cp.y1, cp.y2);
return {
x1: minX - n.size,
y1: minY - n.size,
x2: maxX + n.size,
y2: minY - n.size,
height: maxY - minY + n.size * 2
};
},
/**
* Checks whether a rectangle is axis-aligned.
*
* @param {object} A rectangle defined by two points
* (x1, y1) and (x2, y2).
* @return {boolean} True if the rectangle is axis-aligned.
*/
isAxisAligned: function(r) {
return r.x1 === r.x2 || r.y1 === r.y2;
},
/**
* Compute top points of an axis-aligned rectangle. This is useful in
* cases when the rectangle has been rotated (left, right or bottom up) and
* later operations need to know the top points.
*
* @param {object} An axis-aligned rectangle defined by two points
* (x1, y1), (x2, y2) and height.
* @return {object} A rectangle: two points (x1, y1), (x2, y2) and height.
*/
axisAlignedTopPoints: function(r) {
// Basic
if (r.y1 === r.y2 && r.x1 < r.x2)
return r;
// Rotated to right
if (r.x1 === r.x2 && r.y2 > r.y1)
return {
x1: r.x1 - r.height, y1: r.y1,
x2: r.x1, y2: r.y1,
height: r.height
};
// Rotated to left
if (r.x1 === r.x2 && r.y2 < r.y1)
return {
x1: r.x1, y1: r.y2,
x2: r.x2 + r.height, y2: r.y2,
height: r.height
};
// Bottom's up
return {
x1: r.x2, y1: r.y1 - r.height,
x2: r.x1, y2: r.y1 - r.height,
height: r.height
};
},
/**
* Get coordinates of a rectangle's lower left corner from its top points.
*
* @param {object} A rectangle defined by two points (x1, y1) and (x2, y2).
* @return {object} Coordinates of the corner (x, y).
*/
lowerLeftCoor: function(r) {
var width = (
Math.sqrt(
Math.pow(r.x2 - r.x1, 2) +
Math.pow(r.y2 - r.y1, 2)
)
);
return {
x: r.x1 - (r.y2 - r.y1) * r.height / width,
y: r.y1 + (r.x2 - r.x1) * r.height / width
};
},
/**
* Get coordinates of a rectangle's lower right corner from its top points
* and its lower left corner.
*
* @param {object} A rectangle defined by two points (x1, y1) and (x2, y2).
* @param {object} A corner's coordinates (x, y).
* @return {object} Coordinates of the corner (x, y).
*/
lowerRightCoor: function(r, llc) {
return {
x: llc.x - r.x1 + r.x2,
y: llc.y - r.y1 + r.y2
};
},
/**
* Get the coordinates of all the corners of a rectangle from its top point.
*
* @param {object} A rectangle defined by two points (x1, y1) and (x2, y2).
* @return {array} An array of the four corners' coordinates (x, y).
*/
rectangleCorners: function(r) {
var llc = this.lowerLeftCoor(r),
lrc = this.lowerRightCoor(r, llc);
return [
{x: r.x1, y: r.y1},
{x: r.x2, y: r.y2},
{x: llc.x, y: llc.y},
{x: lrc.x, y: lrc.y}
];
},
/**
* Split a square defined by its boundaries into four.
*
* @param {object} Boundaries of the square (x, y, width, height).
* @return {array} An array containing the four new squares, themselves
* defined by an array of their four corners (x, y).
*/
splitSquare: function(b) {
return [
[
{x: b.x, y: b.y},
{x: b.x + b.width / 2, y: b.y},
{x: b.x, y: b.y + b.height / 2},
{x: b.x + b.width / 2, y: b.y + b.height / 2}
],
[
{x: b.x + b.width / 2, y: b.y},
{x: b.x + b.width, y: b.y},
{x: b.x + b.width / 2, y: b.y + b.height / 2},
{x: b.x + b.width, y: b.y + b.height / 2}
],
[
{x: b.x, y: b.y + b.height / 2},
{x: b.x + b.width / 2, y: b.y + b.height / 2},
{x: b.x, y: b.y + b.height},
{x: b.x + b.width / 2, y: b.y + b.height}
],
[
{x: b.x + b.width / 2, y: b.y + b.height / 2},
{x: b.x + b.width, y: b.y + b.height / 2},
{x: b.x + b.width / 2, y: b.y + b.height},
{x: b.x + b.width, y: b.y + b.height}
]
];
},
/**
* Compute the four axis between corners of rectangle A and corners of
* rectangle B. This is needed later to check an eventual collision.
*
* @param {array} An array of rectangle A's four corners (x, y).
* @param {array} An array of rectangle B's four corners (x, y).
* @return {array} An array of four axis defined by their coordinates (x,y).
*/
axis: function(c1, c2) {
return [
{x: c1[1].x - c1[0].x, y: c1[1].y - c1[0].y},
{x: c1[1].x - c1[3].x, y: c1[1].y - c1[3].y},
{x: c2[0].x - c2[2].x, y: c2[0].y - c2[2].y},
{x: c2[0].x - c2[1].x, y: c2[0].y - c2[1].y}
];
},
/**
* Project a rectangle's corner on an axis.
*
* @param {object} Coordinates of a corner (x, y).
* @param {object} Coordinates of an axis (x, y).
* @return {object} The projection defined by coordinates (x, y).
*/
projection: function(c, a) {
var l = (
(c.x * a.x + c.y * a.y) /
(Math.pow(a.x, 2) + Math.pow(a.y, 2))
);
return {
x: l * a.x,
y: l * a.y
};
},
/**
* Check whether two rectangles collide on one particular axis.
*
* @param {object} An axis' coordinates (x, y).
* @param {array} Rectangle A's corners.
* @param {array} Rectangle B's corners.
* @return {boolean} True if the rectangles collide on the axis.
*/
axisCollision: function(a, c1, c2) {
var sc1 = [],
sc2 = [];
for (var ci = 0; ci < 4; ci++) {
var p1 = this.projection(c1[ci], a),
p2 = this.projection(c2[ci], a);
sc1.push(p1.x * a.x + p1.y * a.y);
sc2.push(p2.x * a.x + p2.y * a.y);
}
var maxc1 = Math.max.apply(Math, sc1),
maxc2 = Math.max.apply(Math, sc2),
minc1 = Math.min.apply(Math, sc1),
minc2 = Math.min.apply(Math, sc2);
return (minc2 <= maxc1 && maxc2 >= minc1);
},
/**
* Check whether two rectangles collide on each one of their four axis. If
* all axis collide, then the two rectangles do collide on the plane.
*
* @param {array} Rectangle A's corners.
* @param {array} Rectangle B's corners.
* @return {boolean} True if the rectangles collide.
*/
collision: function(c1, c2) {
var axis = this.axis(c1, c2),
col = true;
for (var i = 0; i < 4; i++)
col = col && this.axisCollision(axis[i], c1, c2);
return col;
}
};
/**
* Quad Functions
* ------------
*
* The Quadtree functions themselves.
* For each of those functions, we consider that in a splitted quad, the
* index of each node is the following:
* 0: top left
* 1: top right
* 2: bottom left
* 3: bottom right
*
* Moreover, the hereafter quad's philosophy is to consider that if an element
* collides with more than one nodes, this element belongs to each of the
* nodes it collides with where other would let it lie on a higher node.
*/
/**
* Get the index of the node containing the point in the quad
*
* @param {object} point A point defined by coordinates (x, y).
* @param {object} quadBounds Boundaries of the quad (x, y, width, heigth).
* @return {integer} The index of the node containing the point.
*/
function _quadIndex(point, quadBounds) {
var xmp = quadBounds.x + quadBounds.width / 2,
ymp = quadBounds.y + quadBounds.height / 2,
top = (point.y < ymp),
left = (point.x < xmp);
if (top) {
if (left)
return 0;
else
return 1;
}
else {
if (left)
return 2;
else
return 3;
}
}
/**
* Get a list of indexes of nodes containing an axis-aligned rectangle
*
* @param {object} rectangle A rectangle defined by two points (x1, y1),
* (x2, y2) and height.
* @param {array} quadCorners An array of the quad nodes' corners.
* @return {array} An array of indexes containing one to
* four integers.
*/
function _quadIndexes(rectangle, quadCorners) {
var indexes = [];
// Iterating through quads
for (var i = 0; i < 4; i++)
if ((rectangle.x2 >= quadCorners[i][0].x) &&
(rectangle.x1 <= quadCorners[i][1].x) &&
(rectangle.y1 + rectangle.height >= quadCorners[i][0].y) &&
(rectangle.y1 <= quadCorners[i][2].y))
indexes.push(i);
return indexes;
}
/**
* Get a list of indexes of nodes containing a non-axis-aligned rectangle
*
* @param {array} corners An array containing each corner of the
* rectangle defined by its coordinates (x, y).
* @param {array} quadCorners An array of the quad nodes' corners.
* @return {array} An array of indexes containing one to
* four integers.
*/
function _quadCollision(corners, quadCorners) {
var indexes = [];
// Iterating through quads
for (var i = 0; i < 4; i++)
if (_geom.collision(corners, quadCorners[i]))
indexes.push(i);
return indexes;
}
/**
* Subdivide a quad by creating a node at a precise index. The function does
* not generate all four nodes not to potentially create unused nodes.
*
* @param {integer} index The index of the node to create.
* @param {object} quad The quad object to subdivide.
* @return {object} A new quad representing the node created.
*/
function _quadSubdivide(index, quad) {
var next = quad.level + 1,
subw = Math.round(quad.bounds.width / 2),
subh = Math.round(quad.bounds.height / 2),
qx = Math.round(quad.bounds.x),
qy = Math.round(quad.bounds.y),
x,
y;
switch (index) {
case 0:
x = qx;
y = qy;
break;
case 1:
x = qx + subw;
y = qy;
break;
case 2:
x = qx;
y = qy + subh;
break;
case 3:
x = qx + subw;
y = qy + subh;
break;
}
return _quadTree(
{x: x, y: y, width: subw, height: subh},
next,
quad.maxElements,
quad.maxLevel
);
}
/**
* Recursively insert an element into the quadtree. Only points
* with size, i.e. axis-aligned squares, may be inserted with this
* method.
*
* @param {object} el The element to insert in the quadtree.
* @param {object} sizedPoint A sized point defined by two top points
* (x1, y1), (x2, y2) and height.
* @param {object} quad The quad in which to insert the element.
* @return {undefined} The function does not return anything.
*/
function _quadInsert(el, sizedPoint, quad) {
if (quad.level < quad.maxLevel) {
// Searching appropriate quads
var indexes = _quadIndexes(sizedPoint, quad.corners);
// Iterating
for (var i = 0, l = indexes.length; i < l; i++) {
// Subdividing if necessary
if (quad.nodes[indexes[i]] === undefined)
quad.nodes[indexes[i]] = _quadSubdivide(indexes[i], quad);
// Recursion
_quadInsert(el, sizedPoint, quad.nodes[indexes[i]]);
}
}
else {
// Pushing the element in a leaf node
quad.elements.push(el);
}
}
/**
* Recursively retrieve every elements held by the node containing the
* searched point.
*
* @param {object} point The searched point (x, y).
* @param {object} quad The searched quad.
* @return {array} An array of elements contained in the relevant
* node.
*/
function _quadRetrievePoint(point, quad) {
if (quad.level < quad.maxLevel) {
var index = _quadIndex(point, quad.bounds);
// If node does not exist we return an empty list
if (quad.nodes[index] !== undefined) {
return _quadRetrievePoint(point, quad.nodes[index]);
}
else {
return [];
}
}
else {
return quad.elements;
}
}
/**
* Recursively retrieve every elements contained within an rectangular area
* that may or may not be axis-aligned.
*
* @param {object|array} rectData The searched area defined either by
* an array of four corners (x, y) in
* the case of a non-axis-aligned
* rectangle or an object with two top
* points (x1, y1), (x2, y2) and height.
* @param {object} quad The searched quad.
* @param {function} collisionFunc The collision function used to search
* for node indexes.
* @param {array?} els The retrieved elements.
* @return {array} An array of elements contained in the
* area.
*/
function _quadRetrieveArea(rectData, quad, collisionFunc, els) {
els = els || {};
if (quad.level < quad.maxLevel) {
var indexes = collisionFunc(rectData, quad.corners);
for (var i = 0, l = indexes.length; i < l; i++)
if (quad.nodes[indexes[i]] !== undefined)
_quadRetrieveArea(
rectData,
quad.nodes[indexes[i]],
collisionFunc,
els
);
} else
for (var j = 0, m = quad.elements.length; j < m; j++)
if (els[quad.elements[j].id] === undefined)
els[quad.elements[j].id] = quad.elements[j];
return els;
}
/**
* Creates the quadtree object itself.
*
* @param {object} bounds The boundaries of the quad defined by an
* origin (x, y), width and heigth.
* @param {integer} level The level of the quad in the tree.
* @param {integer} maxElements The max number of element in a leaf node.
* @param {integer} maxLevel The max recursion level of the tree.
* @return {object} The quadtree object.
*/
function _quadTree(bounds, level, maxElements, maxLevel) {
return {
level: level || 0,
bounds: bounds,
corners: _geom.splitSquare(bounds),
maxElements: maxElements || 40,
maxLevel: maxLevel || 8,
elements: [],
nodes: []
};
}
/**
* Sigma Quad Constructor
* ----------------------
*
* The edgequad API as exposed to sigma.
*/
/**
* The edgequad core that will become the sigma interface with the quadtree.
*
* property {object} _tree Property holding the quadtree object.
* property {object} _geom Exposition of the _geom namespace for testing.
* property {object} _cache Cache for the area method.
* property {boolean} _enabled Can index and retreive elements.
*/
var edgequad = function() {
this._geom = _geom;
this._tree = null;
this._cache = {
query: false,
result: false
};
this._enabled = true;
};
/**
* Index a graph by inserting its edges into the quadtree.
*
* @param {object} graph A graph instance.
* @param {object} params An object of parameters with at least the quad
* bounds.
* @return {object} The quadtree object.
*
* Parameters:
* ----------
* bounds: {object} boundaries of the quad defined by its origin (x, y)
* width and heigth.
* prefix: {string?} a prefix for edge geometric attributes.
* maxElements: {integer?} the max number of elements in a leaf node.
* maxLevel: {integer?} the max recursion level of the tree.
*/
edgequad.prototype.index = function(graph, params) {
if (!this._enabled)
return this._tree;
// Enforcing presence of boundaries
if (!params.bounds)
throw 'sigma.classes.edgequad.index: bounds information not given.';
// Prefix
var prefix = params.prefix || '',
cp,
source,
target,
n,
e;
// Building the tree
this._tree = _quadTree(
params.bounds,
0,
params.maxElements,
params.maxLevel
);
var edges = graph.edges();
// Inserting graph edges into the tree
for (var i = 0, l = edges.length; i < l; i++) {
source = graph.nodes(edges[i].source);
target = graph.nodes(edges[i].target);
e = {
x1: source[prefix + 'x'],
y1: source[prefix + 'y'],
x2: target[prefix + 'x'],
y2: target[prefix + 'y'],
size: edges[i][prefix + 'size'] || 0
};
// Inserting edge
if (edges[i].type === 'curve' || edges[i].type === 'curvedArrow') {
if (source.id === target.id) {
n = {
x: source[prefix + 'x'],
y: source[prefix + 'y'],
size: source[prefix + 'size'] || 0
};
_quadInsert(
edges[i],
_geom.selfLoopToSquare(n),
this._tree);
}
else {
cp = sigma.utils.getQuadraticControlPoint(e.x1, e.y1, e.x2, e.y2);
_quadInsert(
edges[i],
_geom.quadraticCurveToSquare(e, cp),
this._tree);
}
}
else {
_quadInsert(
edges[i],
_geom.lineToSquare(e),
this._tree);
}
}
// Reset cache:
this._cache = {
query: false,
result: false
};
// remove?
return this._tree;
};
/**
* Retrieve every graph edges held by the quadtree node containing the
* searched point.
*
* @param {number} x of the point.
* @param {number} y of the point.
* @return {array} An array of edges retrieved.
*/
edgequad.prototype.point = function(x, y) {
if (!this._enabled)
return [];
return this._tree ?
_quadRetrievePoint({x: x, y: y}, this._tree) || [] :
[];
};
/**
* Retrieve every graph edges within a rectangular area. The methods keep the
* last area queried in cache for optimization reason and will act differently
* for the same reason if the area is axis-aligned or not.
*
* @param {object} A rectangle defined by two top points (x1, y1), (x2, y2)
* and height.
* @return {array} An array of edges retrieved.
*/
edgequad.prototype.area = function(rect) {
if (!this._enabled)
return [];
var serialized = JSON.stringify(rect),
collisionFunc,
rectData;
// Returning cache?
if (this._cache.query === serialized)
return this._cache.result;
// Axis aligned ?
if (_geom.isAxisAligned(rect)) {
collisionFunc = _quadIndexes;
rectData = _geom.axisAlignedTopPoints(rect);
}
else {
collisionFunc = _quadCollision;
rectData = _geom.rectangleCorners(rect);
}
// Retrieving edges
var edges = this._tree ?
_quadRetrieveArea(
rectData,
this._tree,
collisionFunc
) :
[];
// Object to array
var edgesArray = [];
for (var i in edges)
edgesArray.push(edges[i]);
// Caching
this._cache.query = serialized;
this._cache.result = edgesArray;
return edgesArray;
};
/**
* EXPORT:
* *******
*/
if (typeof this.sigma !== 'undefined') {
this.sigma.classes = this.sigma.classes || {};
this.sigma.classes.edgequad = edgequad;
} else if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports)
exports = module.exports = edgequad;
exports.edgequad = edgequad;
} else
this.edgequad = edgequad;
}).call(this);

View File

@ -0,0 +1,859 @@
;(function(undefined) {
'use strict';
var _methods = Object.create(null),
_indexes = Object.create(null),
_initBindings = Object.create(null),
_methodBindings = Object.create(null),
_methodBeforeBindings = Object.create(null),
_defaultSettings = {
immutable: true,
clone: true
},
_defaultSettingsFunction = function(key) {
return _defaultSettings[key];
};
/**
* The graph constructor. It initializes the data and the indexes, and binds
* the custom indexes and methods to its own scope.
*
* Recognized parameters:
* **********************
* Here is the exhaustive list of every accepted parameters in the settings
* object:
*
* {boolean} clone Indicates if the data have to be cloned in methods
* to add nodes or edges.
* {boolean} immutable Indicates if nodes "id" values and edges "id",
* "source" and "target" values must be set as
* immutable.
*
* @param {?configurable} settings Eventually a settings function.
* @return {graph} The new graph instance.
*/
var graph = function(settings) {
var k,
fn,
data;
/**
* DATA:
* *****
* Every data that is callable from graph methods are stored in this "data"
* object. This object will be served as context for all these methods,
* and it is possible to add other type of data in it.
*/
data = {
/**
* SETTINGS FUNCTION:
* ******************
*/
settings: settings || _defaultSettingsFunction,
/**
* MAIN DATA:
* **********
*/
nodesArray: [],
edgesArray: [],
/**
* GLOBAL INDEXES:
* ***************
* These indexes just index data by ids.
*/
nodesIndex: Object.create(null),
edgesIndex: Object.create(null),
/**
* LOCAL INDEXES:
* **************
* These indexes refer from node to nodes. Each key is an id, and each
* value is the array of the ids of related nodes.
*/
inNeighborsIndex: Object.create(null),
outNeighborsIndex: Object.create(null),
allNeighborsIndex: Object.create(null),
inNeighborsCount: Object.create(null),
outNeighborsCount: Object.create(null),
allNeighborsCount: Object.create(null)
};
// Execute bindings:
for (k in _initBindings)
_initBindings[k].call(data);
// Add methods to both the scope and the data objects:
for (k in _methods) {
fn = __bindGraphMethod(k, data, _methods[k]);
this[k] = fn;
data[k] = fn;
}
};
/**
* A custom tool to bind methods such that function that are bound to it will
* be executed anytime the method is called.
*
* @param {string} methodName The name of the method to bind.
* @param {object} scope The scope where the method must be executed.
* @param {function} fn The method itself.
* @return {function} The new method.
*/
function __bindGraphMethod(methodName, scope, fn) {
var result = function() {
var k,
res;
// Execute "before" bound functions:
for (k in _methodBeforeBindings[methodName])
_methodBeforeBindings[methodName][k].apply(scope, arguments);
// Apply the method:
res = fn.apply(scope, arguments);
// Execute bound functions:
for (k in _methodBindings[methodName])
_methodBindings[methodName][k].apply(scope, arguments);
// Return res:
return res;
};
return result;
}
/**
* This custom tool function removes every pair key/value from an hash. The
* goal is to avoid creating a new object while some other references are
* still hanging in some scopes...
*
* @param {object} obj The object to empty.
* @return {object} The empty object.
*/
function __emptyObject(obj) {
var k;
for (k in obj)
if (!('hasOwnProperty' in obj) || obj.hasOwnProperty(k))
delete obj[k];
return obj;
}
/**
* This global method adds a method that will be bound to the futurly created
* graph instances.
*
* Since these methods will be bound to their scope when the instances are
* created, it does not use the prototype. Because of that, methods have to
* be added before instances are created to make them available.
*
* Here is an example:
*
* > graph.addMethod('getNodesCount', function() {
* > return this.nodesArray.length;
* > });
* >
* > var myGraph = new graph();
* > console.log(myGraph.getNodesCount()); // outputs 0
*
* @param {string} methodName The name of the method.
* @param {function} fn The method itself.
* @return {object} The global graph constructor.
*/
graph.addMethod = function(methodName, fn) {
if (
typeof methodName !== 'string' ||
typeof fn !== 'function' ||
arguments.length !== 2
)
throw 'addMethod: Wrong arguments.';
if (_methods[methodName] || graph[methodName])
throw 'The method "' + methodName + '" already exists.';
_methods[methodName] = fn;
_methodBindings[methodName] = Object.create(null);
_methodBeforeBindings[methodName] = Object.create(null);
return this;
};
/**
* This global method returns true if the method has already been added, and
* false else.
*
* Here are some examples:
*
* > graph.hasMethod('addNode'); // returns true
* > graph.hasMethod('hasMethod'); // returns true
* > graph.hasMethod('unexistingMethod'); // returns false
*
* @param {string} methodName The name of the method.
* @return {boolean} The result.
*/
graph.hasMethod = function(methodName) {
return !!(_methods[methodName] || graph[methodName]);
};
/**
* This global methods attaches a function to a method. Anytime the specified
* method is called, the attached function is called right after, with the
* same arguments and in the same scope. The attached function is called
* right before if the last argument is true, unless the method is the graph
* constructor.
*
* To attach a function to the graph constructor, use 'constructor' as the
* method name (first argument).
*
* The main idea is to have a clean way to keep custom indexes up to date,
* for instance:
*
* > var timesAddNodeCalled = 0;
* > graph.attach('addNode', 'timesAddNodeCalledInc', function() {
* > timesAddNodeCalled++;
* > });
* >
* > var myGraph = new graph();
* > console.log(timesAddNodeCalled); // outputs 0
* >
* > myGraph.addNode({ id: '1' }).addNode({ id: '2' });
* > console.log(timesAddNodeCalled); // outputs 2
*
* The idea for calling a function before is to provide pre-processors, for
* instance:
*
* > var colorPalette = { Person: '#C3CBE1', Place: '#9BDEBD' };
* > graph.attach('addNode', 'applyNodeColorPalette', function(n) {
* > n.color = colorPalette[n.category];
* > }, true);
* >
* > var myGraph = new graph();
* > myGraph.addNode({ id: 'n0', category: 'Person' });
* > console.log(myGraph.nodes('n0').color); // outputs '#C3CBE1'
*
* @param {string} methodName The name of the related method or
* "constructor".
* @param {string} key The key to identify the function to attach.
* @param {function} fn The function to bind.
* @param {boolean} before If true the function is called right before.
* @return {object} The global graph constructor.
*/
graph.attach = function(methodName, key, fn, before) {
if (
typeof methodName !== 'string' ||
typeof key !== 'string' ||
typeof fn !== 'function' ||
arguments.length < 3 ||
arguments.length > 4
)
throw 'attach: Wrong arguments.';
var bindings;
if (methodName === 'constructor')
bindings = _initBindings;
else {
if (before) {
if (!_methodBeforeBindings[methodName])
throw 'The method "' + methodName + '" does not exist.';
bindings = _methodBeforeBindings[methodName];
}
else {
if (!_methodBindings[methodName])
throw 'The method "' + methodName + '" does not exist.';
bindings = _methodBindings[methodName];
}
}
if (bindings[key])
throw 'A function "' + key + '" is already attached ' +
'to the method "' + methodName + '".';
bindings[key] = fn;
return this;
};
/**
* Alias of attach(methodName, key, fn, true).
*/
graph.attachBefore = function(methodName, key, fn) {
return this.attach(methodName, key, fn, true);
};
/**
* This methods is just an helper to deal with custom indexes. It takes as
* arguments the name of the index and an object containing all the different
* functions to bind to the methods.
*
* Here is a basic example, that creates an index to keep the number of nodes
* in the current graph. It also adds a method to provide a getter on that
* new index:
*
* > sigma.classes.graph.addIndex('nodesCount', {
* > constructor: function() {
* > this.nodesCount = 0;
* > },
* > addNode: function() {
* > this.nodesCount++;
* > },
* > dropNode: function() {
* > this.nodesCount--;
* > }
* > });
* >
* > sigma.classes.graph.addMethod('getNodesCount', function() {
* > return this.nodesCount;
* > });
* >
* > var myGraph = new sigma.classes.graph();
* > console.log(myGraph.getNodesCount()); // outputs 0
* >
* > myGraph.addNode({ id: '1' }).addNode({ id: '2' });
* > console.log(myGraph.getNodesCount()); // outputs 2
*
* @param {string} name The name of the index.
* @param {object} bindings The object containing the functions to bind.
* @return {object} The global graph constructor.
*/
graph.addIndex = function(name, bindings) {
if (
typeof name !== 'string' ||
Object(bindings) !== bindings ||
arguments.length !== 2
)
throw 'addIndex: Wrong arguments.';
if (_indexes[name])
throw 'The index "' + name + '" already exists.';
var k;
// Store the bindings:
_indexes[name] = bindings;
// Attach the bindings:
for (k in bindings)
if (typeof bindings[k] !== 'function')
throw 'The bindings must be functions.';
else
graph.attach(k, name, bindings[k]);
return this;
};
/**
* This method adds a node to the graph. The node must be an object, with a
* string under the key "id". Except for this, it is possible to add any
* other attribute, that will be preserved all along the manipulations.
*
* If the graph option "clone" has a truthy value, the node will be cloned
* when added to the graph. Also, if the graph option "immutable" has a
* truthy value, its id will be defined as immutable.
*
* @param {object} node The node to add.
* @return {object} The graph instance.
*/
graph.addMethod('addNode', function(node) {
// Check that the node is an object and has an id:
if (Object(node) !== node || arguments.length !== 1)
throw 'addNode: Wrong arguments.';
if (typeof node.id !== 'string' && typeof node.id !== 'number')
throw 'The node must have a string or number id.';
if (this.nodesIndex[node.id])
throw 'The node "' + node.id + '" already exists.';
var k,
id = node.id,
validNode = Object.create(null);
// Check the "clone" option:
if (this.settings('clone')) {
for (k in node)
if (k !== 'id')
validNode[k] = node[k];
} else
validNode = node;
// Check the "immutable" option:
if (this.settings('immutable'))
Object.defineProperty(validNode, 'id', {
value: id,
enumerable: true
});
else
validNode.id = id;
// Add empty containers for edges indexes:
this.inNeighborsIndex[id] = Object.create(null);
this.outNeighborsIndex[id] = Object.create(null);
this.allNeighborsIndex[id] = Object.create(null);
this.inNeighborsCount[id] = 0;
this.outNeighborsCount[id] = 0;
this.allNeighborsCount[id] = 0;
// Add the node to indexes:
this.nodesArray.push(validNode);
this.nodesIndex[validNode.id] = validNode;
// Return the current instance:
return this;
});
/**
* This method adds an edge to the graph. The edge must be an object, with a
* string under the key "id", and strings under the keys "source" and
* "target" that design existing nodes. Except for this, it is possible to
* add any other attribute, that will be preserved all along the
* manipulations.
*
* If the graph option "clone" has a truthy value, the edge will be cloned
* when added to the graph. Also, if the graph option "immutable" has a
* truthy value, its id, source and target will be defined as immutable.
*
* @param {object} edge The edge to add.
* @return {object} The graph instance.
*/
graph.addMethod('addEdge', function(edge) {
// Check that the edge is an object and has an id:
if (Object(edge) !== edge || arguments.length !== 1)
throw 'addEdge: Wrong arguments.';
if (typeof edge.id !== 'string' && typeof edge.id !== 'number')
throw 'The edge must have a string or number id.';
if ((typeof edge.source !== 'string' && typeof edge.source !== 'number') ||
!this.nodesIndex[edge.source])
throw 'The edge source must have an existing node id.';
if ((typeof edge.target !== 'string' && typeof edge.target !== 'number') ||
!this.nodesIndex[edge.target])
throw 'The edge target must have an existing node id.';
if (this.edgesIndex[edge.id])
throw 'The edge "' + edge.id + '" already exists.';
var k,
validEdge = Object.create(null);
// Check the "clone" option:
if (this.settings('clone')) {
for (k in edge)
if (k !== 'id' && k !== 'source' && k !== 'target')
validEdge[k] = edge[k];
} else
validEdge = edge;
// Check the "immutable" option:
if (this.settings('immutable')) {
Object.defineProperty(validEdge, 'id', {
value: edge.id,
enumerable: true
});
Object.defineProperty(validEdge, 'source', {
value: edge.source,
enumerable: true
});
Object.defineProperty(validEdge, 'target', {
value: edge.target,
enumerable: true
});
} else {
validEdge.id = edge.id;
validEdge.source = edge.source;
validEdge.target = edge.target;
}
// Add the edge to indexes:
this.edgesArray.push(validEdge);
this.edgesIndex[validEdge.id] = validEdge;
if (!this.inNeighborsIndex[validEdge.target][validEdge.source])
this.inNeighborsIndex[validEdge.target][validEdge.source] =
Object.create(null);
this.inNeighborsIndex[validEdge.target][validEdge.source][validEdge.id] =
validEdge;
if (!this.outNeighborsIndex[validEdge.source][validEdge.target])
this.outNeighborsIndex[validEdge.source][validEdge.target] =
Object.create(null);
this.outNeighborsIndex[validEdge.source][validEdge.target][validEdge.id] =
validEdge;
if (!this.allNeighborsIndex[validEdge.source][validEdge.target])
this.allNeighborsIndex[validEdge.source][validEdge.target] =
Object.create(null);
this.allNeighborsIndex[validEdge.source][validEdge.target][validEdge.id] =
validEdge;
if (validEdge.target !== validEdge.source) {
if (!this.allNeighborsIndex[validEdge.target][validEdge.source])
this.allNeighborsIndex[validEdge.target][validEdge.source] =
Object.create(null);
this.allNeighborsIndex[validEdge.target][validEdge.source][validEdge.id] =
validEdge;
}
// Keep counts up to date:
this.inNeighborsCount[validEdge.target]++;
this.outNeighborsCount[validEdge.source]++;
this.allNeighborsCount[validEdge.target]++;
this.allNeighborsCount[validEdge.source]++;
return this;
});
/**
* This method drops a node from the graph. It also removes each edge that is
* bound to it, through the dropEdge method. An error is thrown if the node
* does not exist.
*
* @param {string} id The node id.
* @return {object} The graph instance.
*/
graph.addMethod('dropNode', function(id) {
// Check that the arguments are valid:
if ((typeof id !== 'string' && typeof id !== 'number') ||
arguments.length !== 1)
throw 'dropNode: Wrong arguments.';
if (!this.nodesIndex[id])
throw 'The node "' + id + '" does not exist.';
var i, k, l;
// Remove the node from indexes:
delete this.nodesIndex[id];
for (i = 0, l = this.nodesArray.length; i < l; i++)
if (this.nodesArray[i].id === id) {
this.nodesArray.splice(i, 1);
break;
}
// Remove related edges:
for (i = this.edgesArray.length - 1; i >= 0; i--)
if (this.edgesArray[i].source === id || this.edgesArray[i].target === id)
this.dropEdge(this.edgesArray[i].id);
// Remove related edge indexes:
delete this.inNeighborsIndex[id];
delete this.outNeighborsIndex[id];
delete this.allNeighborsIndex[id];
delete this.inNeighborsCount[id];
delete this.outNeighborsCount[id];
delete this.allNeighborsCount[id];
for (k in this.nodesIndex) {
delete this.inNeighborsIndex[k][id];
delete this.outNeighborsIndex[k][id];
delete this.allNeighborsIndex[k][id];
}
return this;
});
/**
* This method drops an edge from the graph. An error is thrown if the edge
* does not exist.
*
* @param {string} id The edge id.
* @return {object} The graph instance.
*/
graph.addMethod('dropEdge', function(id) {
// Check that the arguments are valid:
if ((typeof id !== 'string' && typeof id !== 'number') ||
arguments.length !== 1)
throw 'dropEdge: Wrong arguments.';
if (!this.edgesIndex[id])
throw 'The edge "' + id + '" does not exist.';
var i, l, edge;
// Remove the edge from indexes:
edge = this.edgesIndex[id];
delete this.edgesIndex[id];
for (i = 0, l = this.edgesArray.length; i < l; i++)
if (this.edgesArray[i].id === id) {
this.edgesArray.splice(i, 1);
break;
}
delete this.inNeighborsIndex[edge.target][edge.source][edge.id];
if (!Object.keys(this.inNeighborsIndex[edge.target][edge.source]).length)
delete this.inNeighborsIndex[edge.target][edge.source];
delete this.outNeighborsIndex[edge.source][edge.target][edge.id];
if (!Object.keys(this.outNeighborsIndex[edge.source][edge.target]).length)
delete this.outNeighborsIndex[edge.source][edge.target];
delete this.allNeighborsIndex[edge.source][edge.target][edge.id];
if (!Object.keys(this.allNeighborsIndex[edge.source][edge.target]).length)
delete this.allNeighborsIndex[edge.source][edge.target];
if (edge.target !== edge.source) {
delete this.allNeighborsIndex[edge.target][edge.source][edge.id];
if (!Object.keys(this.allNeighborsIndex[edge.target][edge.source]).length)
delete this.allNeighborsIndex[edge.target][edge.source];
}
this.inNeighborsCount[edge.target]--;
this.outNeighborsCount[edge.source]--;
this.allNeighborsCount[edge.source]--;
this.allNeighborsCount[edge.target]--;
return this;
});
/**
* This method destroys the current instance. It basically empties each index
* and methods attached to the graph.
*/
graph.addMethod('kill', function() {
// Delete arrays:
this.nodesArray.length = 0;
this.edgesArray.length = 0;
delete this.nodesArray;
delete this.edgesArray;
// Delete indexes:
delete this.nodesIndex;
delete this.edgesIndex;
delete this.inNeighborsIndex;
delete this.outNeighborsIndex;
delete this.allNeighborsIndex;
delete this.inNeighborsCount;
delete this.outNeighborsCount;
delete this.allNeighborsCount;
});
/**
* This method empties the nodes and edges arrays, as well as the different
* indexes.
*
* @return {object} The graph instance.
*/
graph.addMethod('clear', function() {
this.nodesArray.length = 0;
this.edgesArray.length = 0;
// Due to GC issues, I prefer not to create new object. These objects are
// only available from the methods and attached functions, but still, it is
// better to prevent ghost references to unrelevant data...
__emptyObject(this.nodesIndex);
__emptyObject(this.edgesIndex);
__emptyObject(this.nodesIndex);
__emptyObject(this.inNeighborsIndex);
__emptyObject(this.outNeighborsIndex);
__emptyObject(this.allNeighborsIndex);
__emptyObject(this.inNeighborsCount);
__emptyObject(this.outNeighborsCount);
__emptyObject(this.allNeighborsCount);
return this;
});
/**
* This method reads an object and adds the nodes and edges, through the
* proper methods "addNode" and "addEdge".
*
* Here is an example:
*
* > var myGraph = new graph();
* > myGraph.read({
* > nodes: [
* > { id: 'n0' },
* > { id: 'n1' }
* > ],
* > edges: [
* > {
* > id: 'e0',
* > source: 'n0',
* > target: 'n1'
* > }
* > ]
* > });
* >
* > console.log(
* > myGraph.nodes().length,
* > myGraph.edges().length
* > ); // outputs 2 1
*
* @param {object} g The graph object.
* @return {object} The graph instance.
*/
graph.addMethod('read', function(g) {
var i,
a,
l;
a = g.nodes || [];
for (i = 0, l = a.length; i < l; i++)
this.addNode(a[i]);
a = g.edges || [];
for (i = 0, l = a.length; i < l; i++)
this.addEdge(a[i]);
return this;
});
/**
* This methods returns one or several nodes, depending on how it is called.
*
* To get the array of nodes, call "nodes" without argument. To get a
* specific node, call it with the id of the node. The get multiple node,
* call it with an array of ids, and it will return the array of nodes, in
* the same order.
*
* @param {?(string|array)} v Eventually one id, an array of ids.
* @return {object|array} The related node or array of nodes.
*/
graph.addMethod('nodes', function(v) {
// Clone the array of nodes and return it:
if (!arguments.length)
return this.nodesArray.slice(0);
// Return the related node:
if (arguments.length === 1 &&
(typeof v === 'string' || typeof v === 'number'))
return this.nodesIndex[v];
// Return an array of the related node:
if (
arguments.length === 1 &&
Object.prototype.toString.call(v) === '[object Array]'
) {
var i,
l,
a = [];
for (i = 0, l = v.length; i < l; i++)
if (typeof v[i] === 'string' || typeof v[i] === 'number')
a.push(this.nodesIndex[v[i]]);
else
throw 'nodes: Wrong arguments.';
return a;
}
throw 'nodes: Wrong arguments.';
});
/**
* This methods returns the degree of one or several nodes, depending on how
* it is called. It is also possible to get incoming or outcoming degrees
* instead by specifying 'in' or 'out' as a second argument.
*
* @param {string|array} v One id, an array of ids.
* @param {?string} which Which degree is required. Values are 'in',
* 'out', and by default the normal degree.
* @return {number|array} The related degree or array of degrees.
*/
graph.addMethod('degree', function(v, which) {
// Check which degree is required:
which = {
'in': this.inNeighborsCount,
'out': this.outNeighborsCount
}[which || ''] || this.allNeighborsCount;
// Return the related node:
if (typeof v === 'string' || typeof v === 'number')
return which[v];
// Return an array of the related node:
if (Object.prototype.toString.call(v) === '[object Array]') {
var i,
l,
a = [];
for (i = 0, l = v.length; i < l; i++)
if (typeof v[i] === 'string' || typeof v[i] === 'number')
a.push(which[v[i]]);
else
throw 'degree: Wrong arguments.';
return a;
}
throw 'degree: Wrong arguments.';
});
/**
* This methods returns one or several edges, depending on how it is called.
*
* To get the array of edges, call "edges" without argument. To get a
* specific edge, call it with the id of the edge. The get multiple edge,
* call it with an array of ids, and it will return the array of edges, in
* the same order.
*
* @param {?(string|array)} v Eventually one id, an array of ids.
* @return {object|array} The related edge or array of edges.
*/
graph.addMethod('edges', function(v) {
// Clone the array of edges and return it:
if (!arguments.length)
return this.edgesArray.slice(0);
// Return the related edge:
if (arguments.length === 1 &&
(typeof v === 'string' || typeof v === 'number'))
return this.edgesIndex[v];
// Return an array of the related edge:
if (
arguments.length === 1 &&
Object.prototype.toString.call(v) === '[object Array]'
) {
var i,
l,
a = [];
for (i = 0, l = v.length; i < l; i++)
if (typeof v[i] === 'string' || typeof v[i] === 'number')
a.push(this.edgesIndex[v[i]]);
else
throw 'edges: Wrong arguments.';
return a;
}
throw 'edges: Wrong arguments.';
});
/**
* EXPORT:
* *******
*/
if (typeof sigma !== 'undefined') {
sigma.classes = sigma.classes || Object.create(null);
sigma.classes.graph = graph;
} else if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports)
exports = module.exports = graph;
exports.graph = graph;
} else
this.graph = graph;
}).call(this);

View File

@ -0,0 +1,674 @@
;(function(undefined) {
'use strict';
/**
* Sigma Quadtree Module
* =====================
*
* Author: Guillaume Plique (Yomguithereal)
* Version: 0.2
*/
/**
* Quad Geometric Operations
* -------------------------
*
* A useful batch of geometric operations used by the quadtree.
*/
var _geom = {
/**
* Transforms a graph node with x, y and size into an
* axis-aligned square.
*
* @param {object} A graph node with at least a point (x, y) and a size.
* @return {object} A square: two points (x1, y1), (x2, y2) and height.
*/
pointToSquare: function(n) {
return {
x1: n.x - n.size,
y1: n.y - n.size,
x2: n.x + n.size,
y2: n.y - n.size,
height: n.size * 2
};
},
/**
* Checks whether a rectangle is axis-aligned.
*
* @param {object} A rectangle defined by two points
* (x1, y1) and (x2, y2).
* @return {boolean} True if the rectangle is axis-aligned.
*/
isAxisAligned: function(r) {
return r.x1 === r.x2 || r.y1 === r.y2;
},
/**
* Compute top points of an axis-aligned rectangle. This is useful in
* cases when the rectangle has been rotated (left, right or bottom up) and
* later operations need to know the top points.
*
* @param {object} An axis-aligned rectangle defined by two points
* (x1, y1), (x2, y2) and height.
* @return {object} A rectangle: two points (x1, y1), (x2, y2) and height.
*/
axisAlignedTopPoints: function(r) {
// Basic
if (r.y1 === r.y2 && r.x1 < r.x2)
return r;
// Rotated to right
if (r.x1 === r.x2 && r.y2 > r.y1)
return {
x1: r.x1 - r.height, y1: r.y1,
x2: r.x1, y2: r.y1,
height: r.height
};
// Rotated to left
if (r.x1 === r.x2 && r.y2 < r.y1)
return {
x1: r.x1, y1: r.y2,
x2: r.x2 + r.height, y2: r.y2,
height: r.height
};
// Bottom's up
return {
x1: r.x2, y1: r.y1 - r.height,
x2: r.x1, y2: r.y1 - r.height,
height: r.height
};
},
/**
* Get coordinates of a rectangle's lower left corner from its top points.
*
* @param {object} A rectangle defined by two points (x1, y1) and (x2, y2).
* @return {object} Coordinates of the corner (x, y).
*/
lowerLeftCoor: function(r) {
var width = (
Math.sqrt(
Math.pow(r.x2 - r.x1, 2) +
Math.pow(r.y2 - r.y1, 2)
)
);
return {
x: r.x1 - (r.y2 - r.y1) * r.height / width,
y: r.y1 + (r.x2 - r.x1) * r.height / width
};
},
/**
* Get coordinates of a rectangle's lower right corner from its top points
* and its lower left corner.
*
* @param {object} A rectangle defined by two points (x1, y1) and (x2, y2).
* @param {object} A corner's coordinates (x, y).
* @return {object} Coordinates of the corner (x, y).
*/
lowerRightCoor: function(r, llc) {
return {
x: llc.x - r.x1 + r.x2,
y: llc.y - r.y1 + r.y2
};
},
/**
* Get the coordinates of all the corners of a rectangle from its top point.
*
* @param {object} A rectangle defined by two points (x1, y1) and (x2, y2).
* @return {array} An array of the four corners' coordinates (x, y).
*/
rectangleCorners: function(r) {
var llc = this.lowerLeftCoor(r),
lrc = this.lowerRightCoor(r, llc);
return [
{x: r.x1, y: r.y1},
{x: r.x2, y: r.y2},
{x: llc.x, y: llc.y},
{x: lrc.x, y: lrc.y}
];
},
/**
* Split a square defined by its boundaries into four.
*
* @param {object} Boundaries of the square (x, y, width, height).
* @return {array} An array containing the four new squares, themselves
* defined by an array of their four corners (x, y).
*/
splitSquare: function(b) {
return [
[
{x: b.x, y: b.y},
{x: b.x + b.width / 2, y: b.y},
{x: b.x, y: b.y + b.height / 2},
{x: b.x + b.width / 2, y: b.y + b.height / 2}
],
[
{x: b.x + b.width / 2, y: b.y},
{x: b.x + b.width, y: b.y},
{x: b.x + b.width / 2, y: b.y + b.height / 2},
{x: b.x + b.width, y: b.y + b.height / 2}
],
[
{x: b.x, y: b.y + b.height / 2},
{x: b.x + b.width / 2, y: b.y + b.height / 2},
{x: b.x, y: b.y + b.height},
{x: b.x + b.width / 2, y: b.y + b.height}
],
[
{x: b.x + b.width / 2, y: b.y + b.height / 2},
{x: b.x + b.width, y: b.y + b.height / 2},
{x: b.x + b.width / 2, y: b.y + b.height},
{x: b.x + b.width, y: b.y + b.height}
]
];
},
/**
* Compute the four axis between corners of rectangle A and corners of
* rectangle B. This is needed later to check an eventual collision.
*
* @param {array} An array of rectangle A's four corners (x, y).
* @param {array} An array of rectangle B's four corners (x, y).
* @return {array} An array of four axis defined by their coordinates (x,y).
*/
axis: function(c1, c2) {
return [
{x: c1[1].x - c1[0].x, y: c1[1].y - c1[0].y},
{x: c1[1].x - c1[3].x, y: c1[1].y - c1[3].y},
{x: c2[0].x - c2[2].x, y: c2[0].y - c2[2].y},
{x: c2[0].x - c2[1].x, y: c2[0].y - c2[1].y}
];
},
/**
* Project a rectangle's corner on an axis.
*
* @param {object} Coordinates of a corner (x, y).
* @param {object} Coordinates of an axis (x, y).
* @return {object} The projection defined by coordinates (x, y).
*/
projection: function(c, a) {
var l = (
(c.x * a.x + c.y * a.y) /
(Math.pow(a.x, 2) + Math.pow(a.y, 2))
);
return {
x: l * a.x,
y: l * a.y
};
},
/**
* Check whether two rectangles collide on one particular axis.
*
* @param {object} An axis' coordinates (x, y).
* @param {array} Rectangle A's corners.
* @param {array} Rectangle B's corners.
* @return {boolean} True if the rectangles collide on the axis.
*/
axisCollision: function(a, c1, c2) {
var sc1 = [],
sc2 = [];
for (var ci = 0; ci < 4; ci++) {
var p1 = this.projection(c1[ci], a),
p2 = this.projection(c2[ci], a);
sc1.push(p1.x * a.x + p1.y * a.y);
sc2.push(p2.x * a.x + p2.y * a.y);
}
var maxc1 = Math.max.apply(Math, sc1),
maxc2 = Math.max.apply(Math, sc2),
minc1 = Math.min.apply(Math, sc1),
minc2 = Math.min.apply(Math, sc2);
return (minc2 <= maxc1 && maxc2 >= minc1);
},
/**
* Check whether two rectangles collide on each one of their four axis. If
* all axis collide, then the two rectangles do collide on the plane.
*
* @param {array} Rectangle A's corners.
* @param {array} Rectangle B's corners.
* @return {boolean} True if the rectangles collide.
*/
collision: function(c1, c2) {
var axis = this.axis(c1, c2),
col = true;
for (var i = 0; i < 4; i++)
col = col && this.axisCollision(axis[i], c1, c2);
return col;
}
};
/**
* Quad Functions
* ------------
*
* The Quadtree functions themselves.
* For each of those functions, we consider that in a splitted quad, the
* index of each node is the following:
* 0: top left
* 1: top right
* 2: bottom left
* 3: bottom right
*
* Moreover, the hereafter quad's philosophy is to consider that if an element
* collides with more than one nodes, this element belongs to each of the
* nodes it collides with where other would let it lie on a higher node.
*/
/**
* Get the index of the node containing the point in the quad
*
* @param {object} point A point defined by coordinates (x, y).
* @param {object} quadBounds Boundaries of the quad (x, y, width, heigth).
* @return {integer} The index of the node containing the point.
*/
function _quadIndex(point, quadBounds) {
var xmp = quadBounds.x + quadBounds.width / 2,
ymp = quadBounds.y + quadBounds.height / 2,
top = (point.y < ymp),
left = (point.x < xmp);
if (top) {
if (left)
return 0;
else
return 1;
}
else {
if (left)
return 2;
else
return 3;
}
}
/**
* Get a list of indexes of nodes containing an axis-aligned rectangle
*
* @param {object} rectangle A rectangle defined by two points (x1, y1),
* (x2, y2) and height.
* @param {array} quadCorners An array of the quad nodes' corners.
* @return {array} An array of indexes containing one to
* four integers.
*/
function _quadIndexes(rectangle, quadCorners) {
var indexes = [];
// Iterating through quads
for (var i = 0; i < 4; i++)
if ((rectangle.x2 >= quadCorners[i][0].x) &&
(rectangle.x1 <= quadCorners[i][1].x) &&
(rectangle.y1 + rectangle.height >= quadCorners[i][0].y) &&
(rectangle.y1 <= quadCorners[i][2].y))
indexes.push(i);
return indexes;
}
/**
* Get a list of indexes of nodes containing a non-axis-aligned rectangle
*
* @param {array} corners An array containing each corner of the
* rectangle defined by its coordinates (x, y).
* @param {array} quadCorners An array of the quad nodes' corners.
* @return {array} An array of indexes containing one to
* four integers.
*/
function _quadCollision(corners, quadCorners) {
var indexes = [];
// Iterating through quads
for (var i = 0; i < 4; i++)
if (_geom.collision(corners, quadCorners[i]))
indexes.push(i);
return indexes;
}
/**
* Subdivide a quad by creating a node at a precise index. The function does
* not generate all four nodes not to potentially create unused nodes.
*
* @param {integer} index The index of the node to create.
* @param {object} quad The quad object to subdivide.
* @return {object} A new quad representing the node created.
*/
function _quadSubdivide(index, quad) {
var next = quad.level + 1,
subw = Math.round(quad.bounds.width / 2),
subh = Math.round(quad.bounds.height / 2),
qx = Math.round(quad.bounds.x),
qy = Math.round(quad.bounds.y),
x,
y;
switch (index) {
case 0:
x = qx;
y = qy;
break;
case 1:
x = qx + subw;
y = qy;
break;
case 2:
x = qx;
y = qy + subh;
break;
case 3:
x = qx + subw;
y = qy + subh;
break;
}
return _quadTree(
{x: x, y: y, width: subw, height: subh},
next,
quad.maxElements,
quad.maxLevel
);
}
/**
* Recursively insert an element into the quadtree. Only points
* with size, i.e. axis-aligned squares, may be inserted with this
* method.
*
* @param {object} el The element to insert in the quadtree.
* @param {object} sizedPoint A sized point defined by two top points
* (x1, y1), (x2, y2) and height.
* @param {object} quad The quad in which to insert the element.
* @return {undefined} The function does not return anything.
*/
function _quadInsert(el, sizedPoint, quad) {
if (quad.level < quad.maxLevel) {
// Searching appropriate quads
var indexes = _quadIndexes(sizedPoint, quad.corners);
// Iterating
for (var i = 0, l = indexes.length; i < l; i++) {
// Subdividing if necessary
if (quad.nodes[indexes[i]] === undefined)
quad.nodes[indexes[i]] = _quadSubdivide(indexes[i], quad);
// Recursion
_quadInsert(el, sizedPoint, quad.nodes[indexes[i]]);
}
}
else {
// Pushing the element in a leaf node
quad.elements.push(el);
}
}
/**
* Recursively retrieve every elements held by the node containing the
* searched point.
*
* @param {object} point The searched point (x, y).
* @param {object} quad The searched quad.
* @return {array} An array of elements contained in the relevant
* node.
*/
function _quadRetrievePoint(point, quad) {
if (quad.level < quad.maxLevel) {
var index = _quadIndex(point, quad.bounds);
// If node does not exist we return an empty list
if (quad.nodes[index] !== undefined) {
return _quadRetrievePoint(point, quad.nodes[index]);
}
else {
return [];
}
}
else {
return quad.elements;
}
}
/**
* Recursively retrieve every elements contained within an rectangular area
* that may or may not be axis-aligned.
*
* @param {object|array} rectData The searched area defined either by
* an array of four corners (x, y) in
* the case of a non-axis-aligned
* rectangle or an object with two top
* points (x1, y1), (x2, y2) and height.
* @param {object} quad The searched quad.
* @param {function} collisionFunc The collision function used to search
* for node indexes.
* @param {array?} els The retrieved elements.
* @return {array} An array of elements contained in the
* area.
*/
function _quadRetrieveArea(rectData, quad, collisionFunc, els) {
els = els || {};
if (quad.level < quad.maxLevel) {
var indexes = collisionFunc(rectData, quad.corners);
for (var i = 0, l = indexes.length; i < l; i++)
if (quad.nodes[indexes[i]] !== undefined)
_quadRetrieveArea(
rectData,
quad.nodes[indexes[i]],
collisionFunc,
els
);
} else
for (var j = 0, m = quad.elements.length; j < m; j++)
if (els[quad.elements[j].id] === undefined)
els[quad.elements[j].id] = quad.elements[j];
return els;
}
/**
* Creates the quadtree object itself.
*
* @param {object} bounds The boundaries of the quad defined by an
* origin (x, y), width and heigth.
* @param {integer} level The level of the quad in the tree.
* @param {integer} maxElements The max number of element in a leaf node.
* @param {integer} maxLevel The max recursion level of the tree.
* @return {object} The quadtree object.
*/
function _quadTree(bounds, level, maxElements, maxLevel) {
return {
level: level || 0,
bounds: bounds,
corners: _geom.splitSquare(bounds),
maxElements: maxElements || 20,
maxLevel: maxLevel || 4,
elements: [],
nodes: []
};
}
/**
* Sigma Quad Constructor
* ----------------------
*
* The quad API as exposed to sigma.
*/
/**
* The quad core that will become the sigma interface with the quadtree.
*
* property {object} _tree Property holding the quadtree object.
* property {object} _geom Exposition of the _geom namespace for testing.
* property {object} _cache Cache for the area method.
*/
var quad = function() {
this._geom = _geom;
this._tree = null;
this._cache = {
query: false,
result: false
};
};
/**
* Index a graph by inserting its nodes into the quadtree.
*
* @param {array} nodes An array of nodes to index.
* @param {object} params An object of parameters with at least the quad
* bounds.
* @return {object} The quadtree object.
*
* Parameters:
* ----------
* bounds: {object} boundaries of the quad defined by its origin (x, y)
* width and heigth.
* prefix: {string?} a prefix for node geometric attributes.
* maxElements: {integer?} the max number of elements in a leaf node.
* maxLevel: {integer?} the max recursion level of the tree.
*/
quad.prototype.index = function(nodes, params) {
// Enforcing presence of boundaries
if (!params.bounds)
throw 'sigma.classes.quad.index: bounds information not given.';
// Prefix
var prefix = params.prefix || '';
// Building the tree
this._tree = _quadTree(
params.bounds,
0,
params.maxElements,
params.maxLevel
);
// Inserting graph nodes into the tree
for (var i = 0, l = nodes.length; i < l; i++) {
// Inserting node
_quadInsert(
nodes[i],
_geom.pointToSquare({
x: nodes[i][prefix + 'x'],
y: nodes[i][prefix + 'y'],
size: nodes[i][prefix + 'size']
}),
this._tree
);
}
// Reset cache:
this._cache = {
query: false,
result: false
};
// remove?
return this._tree;
};
/**
* Retrieve every graph nodes held by the quadtree node containing the
* searched point.
*
* @param {number} x of the point.
* @param {number} y of the point.
* @return {array} An array of nodes retrieved.
*/
quad.prototype.point = function(x, y) {
return this._tree ?
_quadRetrievePoint({x: x, y: y}, this._tree) || [] :
[];
};
/**
* Retrieve every graph nodes within a rectangular area. The methods keep the
* last area queried in cache for optimization reason and will act differently
* for the same reason if the area is axis-aligned or not.
*
* @param {object} A rectangle defined by two top points (x1, y1), (x2, y2)
* and height.
* @return {array} An array of nodes retrieved.
*/
quad.prototype.area = function(rect) {
var serialized = JSON.stringify(rect),
collisionFunc,
rectData;
// Returning cache?
if (this._cache.query === serialized)
return this._cache.result;
// Axis aligned ?
if (_geom.isAxisAligned(rect)) {
collisionFunc = _quadIndexes;
rectData = _geom.axisAlignedTopPoints(rect);
}
else {
collisionFunc = _quadCollision;
rectData = _geom.rectangleCorners(rect);
}
// Retrieving nodes
var nodes = this._tree ?
_quadRetrieveArea(
rectData,
this._tree,
collisionFunc
) :
[];
// Object to array
var nodesArray = [];
for (var i in nodes)
nodesArray.push(nodes[i]);
// Caching
this._cache.query = serialized;
this._cache.result = nodesArray;
return nodesArray;
};
/**
* EXPORT:
* *******
*/
if (typeof this.sigma !== 'undefined') {
this.sigma.classes = this.sigma.classes || {};
this.sigma.classes.quad = quad;
} else if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports)
exports = module.exports = quad;
exports.quad = quad;
} else
this.quad = quad;
}).call(this);

View File

@ -0,0 +1,984 @@
/**
* conrad.js is a tiny JavaScript jobs scheduler,
*
* Version: 0.1.0
* Sources: http://github.com/jacomyal/conrad.js
* Doc: http://github.com/jacomyal/conrad.js#readme
*
* License:
* --------
* Copyright © 2013 Alexis Jacomy, Sciences-Po médialab
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* The Software is provided "as is", without warranty of any kind, express or
* implied, including but not limited to the warranties of merchantability,
* fitness for a particular purpose and noninfringement. In no event shall the
* authors or copyright holders be liable for any claim, damages or other
* liability, whether in an action of contract, tort or otherwise, arising
* from, out of or in connection with the software or the use or other dealings
* in the Software.
*/
(function(global) {
'use strict';
// Check that conrad.js has not been loaded yet:
if (global.conrad)
throw new Error('conrad already exists');
/**
* PRIVATE VARIABLES:
* ******************
*/
/**
* A flag indicating whether conrad is running or not.
*
* @type {Number}
*/
var _lastFrameTime;
/**
* A flag indicating whether conrad is running or not.
*
* @type {Boolean}
*/
var _isRunning = false;
/**
* The hash of registered jobs. Each job must at least have a unique ID
* under the key "id" and a function under the key "job". This hash
* contains each running job and each waiting job.
*
* @type {Object}
*/
var _jobs = {};
/**
* The hash of currently running jobs.
*
* @type {Object}
*/
var _runningJobs = {};
/**
* The array of currently running jobs, sorted by priority.
*
* @type {Array}
*/
var _sortedByPriorityJobs = [];
/**
* The array of currently waiting jobs.
*
* @type {Object}
*/
var _waitingJobs = {};
/**
* The array of finished jobs. They are stored in an array, since two jobs
* with the same "id" can happen at two different times.
*
* @type {Array}
*/
var _doneJobs = [];
/**
* A dirty flag to keep conrad from starting: Indeed, when addJob() is called
* with several jobs, conrad must be started only at the end. This flag keeps
* me from duplicating the code that effectively adds a job.
*
* @type {Boolean}
*/
var _noStart = false;
/**
* An hash containing some global settings about how conrad.js should
* behave.
*
* @type {Object}
*/
var _parameters = {
frameDuration: 20,
history: true
};
/**
* This object contains every handlers bound to conrad events. It does not
* requirea any DOM implementation, since the events are all JavaScript.
*
* @type {Object}
*/
var _handlers = Object.create(null);
/**
* PRIVATE FUNCTIONS:
* ******************
*/
/**
* Will execute the handler everytime that the indicated event (or the
* indicated events) will be triggered.
*
* @param {string|array|object} events The name of the event (or the events
* separated by spaces).
* @param {function(Object)} handler The handler to bind.
* @return {Object} Returns conrad.
*/
function _bind(events, handler) {
var i,
i_end,
event,
eArray;
if (!arguments.length)
return;
else if (
arguments.length === 1 &&
Object(arguments[0]) === arguments[0]
)
for (events in arguments[0])
_bind(events, arguments[0][events]);
else if (arguments.length > 1) {
eArray =
Array.isArray(events) ?
events :
events.split(/ /);
for (i = 0, i_end = eArray.length; i !== i_end; i += 1) {
event = eArray[i];
if (!_handlers[event])
_handlers[event] = [];
// Using an object instead of directly the handler will make possible
// later to add flags
_handlers[event].push({
handler: handler
});
}
}
}
/**
* Removes the handler from a specified event (or specified events).
*
* @param {?string} events The name of the event (or the events
* separated by spaces). If undefined,
* then all handlers are removed.
* @param {?function(Object)} handler The handler to unbind. If undefined,
* each handler bound to the event or the
* events will be removed.
* @return {Object} Returns conrad.
*/
function _unbind(events, handler) {
var i,
i_end,
j,
j_end,
a,
event,
eArray = Array.isArray(events) ?
events :
events.split(/ /);
if (!arguments.length)
_handlers = Object.create(null);
else if (handler) {
for (i = 0, i_end = eArray.length; i !== i_end; i += 1) {
event = eArray[i];
if (_handlers[event]) {
a = [];
for (j = 0, j_end = _handlers[event].length; j !== j_end; j += 1)
if (_handlers[event][j].handler !== handler)
a.push(_handlers[event][j]);
_handlers[event] = a;
}
if (_handlers[event] && _handlers[event].length === 0)
delete _handlers[event];
}
} else
for (i = 0, i_end = eArray.length; i !== i_end; i += 1)
delete _handlers[eArray[i]];
}
/**
* Executes each handler bound to the event.
*
* @param {string} events The name of the event (or the events separated
* by spaces).
* @param {?Object} data The content of the event (optional).
* @return {Object} Returns conrad.
*/
function _dispatch(events, data) {
var i,
j,
i_end,
j_end,
event,
eventName,
eArray = Array.isArray(events) ?
events :
events.split(/ /);
data = data === undefined ? {} : data;
for (i = 0, i_end = eArray.length; i !== i_end; i += 1) {
eventName = eArray[i];
if (_handlers[eventName]) {
event = {
type: eventName,
data: data || {}
};
for (j = 0, j_end = _handlers[eventName].length; j !== j_end; j += 1)
try {
_handlers[eventName][j].handler(event);
} catch (e) {}
}
}
}
/**
* Executes the most prioritary job once, and deals with filling the stats
* (done, time, averageTime, currentTime, etc...).
*
* @return {?Object} Returns the job object if it has to be killed, null else.
*/
function _executeFirstJob() {
var i,
l,
test,
kill,
pushed = false,
time = __dateNow(),
job = _sortedByPriorityJobs.shift();
// Execute the job and look at the result:
test = job.job();
// Deal with stats:
time = __dateNow() - time;
job.done++;
job.time += time;
job.currentTime += time;
job.weightTime = job.currentTime / (job.weight || 1);
job.averageTime = job.time / job.done;
// Check if the job has to be killed:
kill = job.count ? (job.count <= job.done) : !test;
// Reset priorities:
if (!kill) {
for (i = 0, l = _sortedByPriorityJobs.length; i < l; i++)
if (_sortedByPriorityJobs[i].weightTime > job.weightTime) {
_sortedByPriorityJobs.splice(i, 0, job);
pushed = true;
break;
}
if (!pushed)
_sortedByPriorityJobs.push(job);
}
return kill ? job : null;
}
/**
* Activates a job, by adding it to the _runningJobs object and the
* _sortedByPriorityJobs array. It also initializes its currentTime value.
*
* @param {Object} job The job to activate.
*/
function _activateJob(job) {
var l = _sortedByPriorityJobs.length;
// Add the job to the running jobs:
_runningJobs[job.id] = job;
job.status = 'running';
// Add the job to the priorities:
if (l) {
job.weightTime = _sortedByPriorityJobs[l - 1].weightTime;
job.currentTime = job.weightTime * (job.weight || 1);
}
// Initialize the job and dispatch:
job.startTime = __dateNow();
_dispatch('jobStarted', __clone(job));
_sortedByPriorityJobs.push(job);
}
/**
* The main loop of conrad.js:
* . It executes job such that they all occupate the same processing time.
* . It stops jobs that do not need to be executed anymore.
* . It triggers callbacks when it is relevant.
* . It starts waiting jobs when they need to be started.
* . It injects frames to keep a constant frapes per second ratio.
* . It stops itself when there are no more jobs to execute.
*/
function _loop() {
var k,
o,
l,
job,
time,
deadJob;
// Deal with the newly added jobs (the _jobs object):
for (k in _jobs) {
job = _jobs[k];
if (job.after)
_waitingJobs[k] = job;
else
_activateJob(job);
delete _jobs[k];
}
// Set the _isRunning flag to false if there are no running job:
_isRunning = !!_sortedByPriorityJobs.length;
// Deal with the running jobs (the _runningJobs object):
while (
_sortedByPriorityJobs.length &&
__dateNow() - _lastFrameTime < _parameters.frameDuration
) {
deadJob = _executeFirstJob();
// Deal with the case where the job has ended:
if (deadJob) {
_killJob(deadJob.id);
// Check for waiting jobs:
for (k in _waitingJobs)
if (_waitingJobs[k].after === deadJob.id) {
_activateJob(_waitingJobs[k]);
delete _waitingJobs[k];
}
}
}
// Check if conrad still has jobs to deal with, and kill it if not:
if (_isRunning) {
// Update the _lastFrameTime:
_lastFrameTime = __dateNow();
_dispatch('enterFrame');
setTimeout(_loop, 0);
} else
_dispatch('stop');
}
/**
* Adds one or more jobs, and starts the loop if no job was running before. A
* job is at least a unique string "id" and a function, and there are some
* parameters that you can specify for each job to modify the way conrad will
* execute it. If a job is added with the "id" of another job that is waiting
* or still running, an error will be thrown.
*
* When a job is added, it is referenced in the _jobs object, by its id.
* Then, if it has to be executed right now, it will be also referenced in
* the _runningJobs object. If it has to wait, then it will be added into the
* _waitingJobs object, until it can start.
*
* Keep reading this documentation to see how to call this method.
*
* @return {Object} Returns conrad.
*
* Adding one job:
* ***************
* Basically, a job is defined by its string id and a function (the job). It
* is also possible to add some parameters:
*
* > conrad.addJob('myJobId', myJobFunction);
* > conrad.addJob('myJobId', {
* > job: myJobFunction,
* > someParameter: someValue
* > });
* > conrad.addJob({
* > id: 'myJobId',
* > job: myJobFunction,
* > someParameter: someValue
* > });
*
* Adding several jobs:
* ********************
* When adding several jobs at the same time, it is possible to specify
* parameters for each one individually or for all:
*
* > conrad.addJob([
* > {
* > id: 'myJobId1',
* > job: myJobFunction1,
* > someParameter1: someValue1
* > },
* > {
* > id: 'myJobId2',
* > job: myJobFunction2,
* > someParameter2: someValue2
* > }
* > ], {
* > someCommonParameter: someCommonValue
* > });
* > conrad.addJob({
* > myJobId1: {,
* > job: myJobFunction1,
* > someParameter1: someValue1
* > },
* > myJobId2: {,
* > job: myJobFunction2,
* > someParameter2: someValue2
* > }
* > }, {
* > someCommonParameter: someCommonValue
* > });
* > conrad.addJob({
* > myJobId1: myJobFunction1,
* > myJobId2: myJobFunction2
* > }, {
* > someCommonParameter: someCommonValue
* > });
*
* Recognized parameters:
* **********************
* Here is the exhaustive list of every accepted parameters:
*
* {?Function} end A callback to execute when the job is ended. It is
* not executed if the job is killed instead of ended
* "naturally".
* {?Integer} count The number of time the job has to be executed.
* {?Number} weight If specified, the job will be executed as it was
* added "weight" times.
* {?String} after The id of another job (eventually not added yet).
* If specified, this job will start only when the
* specified "after" job is ended.
*/
function _addJob(v1, v2) {
var i,
l,
o;
// Array of jobs:
if (Array.isArray(v1)) {
// Keep conrad to start until the last job is added:
_noStart = true;
for (i = 0, l = v1.length; i < l; i++)
_addJob(v1[i].id, __extend(v1[i], v2));
_noStart = false;
if (!_isRunning) {
// Update the _lastFrameTime:
_lastFrameTime = __dateNow();
_dispatch('start');
_loop();
}
} else if (typeof v1 === 'object') {
// One job (object):
if (typeof v1.id === 'string')
_addJob(v1.id, v1);
// Hash of jobs:
else {
// Keep conrad to start until the last job is added:
_noStart = true;
for (i in v1)
if (typeof v1[i] === 'function')
_addJob(i, __extend({
job: v1[i]
}, v2));
else
_addJob(i, __extend(v1[i], v2));
_noStart = false;
if (!_isRunning) {
// Update the _lastFrameTime:
_lastFrameTime = __dateNow();
_dispatch('start');
_loop();
}
}
// One job (string, *):
} else if (typeof v1 === 'string') {
if (_hasJob(v1))
throw new Error(
'[conrad.addJob] Job with id "' + v1 + '" already exists.'
);
// One job (string, function):
if (typeof v2 === 'function') {
o = {
id: v1,
done: 0,
time: 0,
status: 'waiting',
currentTime: 0,
averageTime: 0,
weightTime: 0,
job: v2
};
// One job (string, object):
} else if (typeof v2 === 'object') {
o = __extend(
{
id: v1,
done: 0,
time: 0,
status: 'waiting',
currentTime: 0,
averageTime: 0,
weightTime: 0
},
v2
);
// If none of those cases, throw an error:
} else
throw new Error('[conrad.addJob] Wrong arguments.');
// Effectively add the job:
_jobs[v1] = o;
_dispatch('jobAdded', __clone(o));
// Check if the loop has to be started:
if (!_isRunning && !_noStart) {
// Update the _lastFrameTime:
_lastFrameTime = __dateNow();
_dispatch('start');
_loop();
}
// If none of those cases, throw an error:
} else
throw new Error('[conrad.addJob] Wrong arguments.');
return this;
}
/**
* Kills one or more jobs, indicated by their ids. It is only possible to
* kill running jobs or waiting jobs. If you try to kill a job that does not
* exists or that is already killed, a warning will be thrown.
*
* @param {Array|String} v1 A string job id or an array of job ids.
* @return {Object} Returns conrad.
*/
function _killJob(v1) {
var i,
l,
k,
a,
job,
found = false;
// Array of job ids:
if (Array.isArray(v1))
for (i = 0, l = v1.length; i < l; i++)
_killJob(v1[i]);
// One job's id:
else if (typeof v1 === 'string') {
a = [_runningJobs, _waitingJobs, _jobs];
// Remove the job from the hashes:
for (i = 0, l = a.length; i < l; i++)
if (v1 in a[i]) {
job = a[i][v1];
if (_parameters.history) {
job.status = 'done';
_doneJobs.push(job);
}
_dispatch('jobEnded', __clone(job));
delete a[i][v1];
if (typeof job.end === 'function')
job.end();
found = true;
}
// Remove the priorities array:
a = _sortedByPriorityJobs;
for (i = 0, l = a.length; i < l; i++)
if (a[i].id === v1) {
a.splice(i, 1);
break;
}
if (!found)
throw new Error('[conrad.killJob] Job "' + v1 + '" not found.');
// If none of those cases, throw an error:
} else
throw new Error('[conrad.killJob] Wrong arguments.');
return this;
}
/**
* Kills every running, waiting, and just added jobs.
*
* @return {Object} Returns conrad.
*/
function _killAll() {
var k,
jobs = __extend(_jobs, _runningJobs, _waitingJobs);
// Take every jobs and push them into the _doneJobs object:
if (_parameters.history)
for (k in jobs) {
jobs[k].status = 'done';
_doneJobs.push(jobs[k]);
if (typeof jobs[k].end === 'function')
jobs[k].end();
}
// Reinitialize the different jobs lists:
_jobs = {};
_waitingJobs = {};
_runningJobs = {};
_sortedByPriorityJobs = [];
// In case some jobs are added right after the kill:
_isRunning = false;
return this;
}
/**
* Returns true if a job with the specified id is currently running or
* waiting, and false else.
*
* @param {String} id The id of the job.
* @return {?Object} Returns the job object if it exists.
*/
function _hasJob(id) {
var job = _jobs[id] || _runningJobs[id] || _waitingJobs[id];
return job ? __extend(job) : null;
}
/**
* This method will set the setting specified by "v1" to the value specified
* by "v2" if both are given, and else return the current value of the
* settings "v1".
*
* @param {String} v1 The name of the property.
* @param {?*} v2 Eventually, a value to set to the specified
* property.
* @return {Object|*} Returns the specified settings value if "v2" is not
* given, and conrad else.
*/
function _settings(v1, v2) {
var o;
if (typeof a1 === 'string' && arguments.length === 1)
return _parameters[a1];
else {
o = (typeof a1 === 'object' && arguments.length === 1) ?
a1 || {} :
{};
if (typeof a1 === 'string')
o[a1] = a2;
for (var k in o)
if (o[k] !== undefined)
_parameters[k] = o[k];
else
delete _parameters[k];
return this;
}
}
/**
* Returns true if conrad is currently running, and false else.
*
* @return {Boolean} Returns _isRunning.
*/
function _getIsRunning() {
return _isRunning;
}
/**
* Unreference every jobs that are stored in the _doneJobs object. It will
* not be possible anymore to get stats about these jobs, but it will release
* the memory.
*
* @return {Object} Returns conrad.
*/
function _clearHistory() {
_doneJobs = [];
return this;
}
/**
* Returns a snapshot of every data about jobs that wait to be started, are
* currently running or are done.
*
* It is possible to get only running, waiting or done jobs by giving
* "running", "waiting" or "done" as fist argument.
*
* It is also possible to get every job with a specified id by giving it as
* first argument. Also, using a RegExp instead of an id will return every
* jobs whose ids match the RegExp. And these two last use cases work as well
* by giving before "running", "waiting" or "done".
*
* @return {Array} The array of the matching jobs.
*
* Some call examples:
* *******************
* > conrad.getStats('running')
* > conrad.getStats('waiting')
* > conrad.getStats('done')
* > conrad.getStats('myJob')
* > conrad.getStats(/test/)
* > conrad.getStats('running', 'myRunningJob')
* > conrad.getStats('running', /test/)
*/
function _getStats(v1, v2) {
var a,
k,
i,
l,
stats,
pattern,
isPatternString;
if (!arguments.length) {
stats = [];
for (k in _jobs)
stats.push(_jobs[k]);
for (k in _waitingJobs)
stats.push(_waitingJobs[k]);
for (k in _runningJobs)
stats.push(_runningJobs[k]);
stats = stats.concat(_doneJobs);
}
if (typeof v1 === 'string')
switch (v1) {
case 'waiting':
stats = __objectValues(_waitingJobs);
break;
case 'running':
stats = __objectValues(_runningJobs);
break;
case 'done':
stats = _doneJobs;
break;
default:
pattern = v1;
}
if (v1 instanceof RegExp)
pattern = v1;
if (!pattern && (typeof v2 === 'string' || v2 instanceof RegExp))
pattern = v2;
// Filter jobs if a pattern is given:
if (pattern) {
isPatternString = typeof pattern === 'string';
if (stats instanceof Array) {
a = stats;
} else if (typeof stats === 'object') {
a = [];
for (k in stats)
a = a.concat(stats[k]);
} else {
a = [];
for (k in _jobs)
a.push(_jobs[k]);
for (k in _waitingJobs)
a.push(_waitingJobs[k]);
for (k in _runningJobs)
a.push(_runningJobs[k]);
a = a.concat(_doneJobs);
}
stats = [];
for (i = 0, l = a.length; i < l; i++)
if (isPatternString ? a[i].id === pattern : a[i].id.match(pattern))
stats.push(a[i]);
}
return __clone(stats);
}
/**
* TOOLS FUNCTIONS:
* ****************
*/
/**
* This function takes any number of objects as arguments, copies from each
* of these objects each pair key/value into a new object, and finally
* returns this object.
*
* The arguments are parsed from the last one to the first one, such that
* when two objects have keys in common, the "earliest" object wins.
*
* Example:
* ********
* > var o1 = {
* > a: 1,
* > b: 2,
* > c: '3'
* > },
* > o2 = {
* > c: '4',
* > d: [ 5 ]
* > };
* > __extend(o1, o2);
* > // Returns: {
* > // a: 1,
* > // b: 2,
* > // c: '3',
* > // d: [ 5 ]
* > // };
*
* @param {Object+} Any number of objects.
* @return {Object} The merged object.
*/
function __extend() {
var i,
k,
res = {},
l = arguments.length;
for (i = l - 1; i >= 0; i--)
for (k in arguments[i])
res[k] = arguments[i][k];
return res;
}
/**
* This function simply clones an object. This object must contain only
* objects, arrays and immutable values. Since it is not public, it does not
* deal with cyclic references, DOM elements and instantiated objects - so
* use it carefully.
*
* @param {Object} The object to clone.
* @return {Object} The clone.
*/
function __clone(item) {
var result, i, k, l;
if (!item)
return item;
if (Array.isArray(item)) {
result = [];
for (i = 0, l = item.length; i < l; i++)
result.push(__clone(item[i]));
} else if (typeof item === 'object') {
result = {};
for (i in item)
result[i] = __clone(item[i]);
} else
result = item;
return result;
}
/**
* Returns an array containing the values of an object.
*
* @param {Object} The object.
* @return {Array} The array of values.
*/
function __objectValues(o) {
var k,
a = [];
for (k in o)
a.push(o[k]);
return a;
}
/**
* A short "Date.now()" polyfill.
*
* @return {Number} The current time (in ms).
*/
function __dateNow() {
return Date.now ? Date.now() : new Date().getTime();
}
/**
* Polyfill for the Array.isArray function:
*/
if (!Array.isArray)
Array.isArray = function(v) {
return Object.prototype.toString.call(v) === '[object Array]';
};
/**
* EXPORT PUBLIC API:
* ******************
*/
var conrad = {
hasJob: _hasJob,
addJob: _addJob,
killJob: _killJob,
killAll: _killAll,
settings: _settings,
getStats: _getStats,
isRunning: _getIsRunning,
clearHistory: _clearHistory,
// Events management:
bind: _bind,
unbind: _unbind,
// Version:
version: '0.1.0'
};
if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports)
exports = module.exports = conrad;
exports.conrad = conrad;
}
global.conrad = conrad;
})(this);

View File

@ -0,0 +1,35 @@
;(function(undefined) {
'use strict';
if (typeof sigma === 'undefined')
throw 'sigma is not declared';
// Initialize packages:
sigma.utils.pkg('sigma.middlewares');
/**
* This middleware will just copy the graphic properties.
*
* @param {?string} readPrefix The read prefix.
* @param {?string} writePrefix The write prefix.
*/
sigma.middlewares.copy = function(readPrefix, writePrefix) {
var i,
l,
a;
if (writePrefix + '' === readPrefix + '')
return;
a = this.graph.nodes();
for (i = 0, l = a.length; i < l; i++) {
a[i][writePrefix + 'x'] = a[i][readPrefix + 'x'];
a[i][writePrefix + 'y'] = a[i][readPrefix + 'y'];
a[i][writePrefix + 'size'] = a[i][readPrefix + 'size'];
}
a = this.graph.edges();
for (i = 0, l = a.length; i < l; i++)
a[i][writePrefix + 'size'] = a[i][readPrefix + 'size'];
};
}).call(this);

View File

@ -0,0 +1,189 @@
;(function(undefined) {
'use strict';
if (typeof sigma === 'undefined')
throw 'sigma is not declared';
// Initialize packages:
sigma.utils.pkg('sigma.middlewares');
sigma.utils.pkg('sigma.utils');
/**
* This middleware will rescale the graph such that it takes an optimal space
* on the renderer.
*
* As each middleware, this function is executed in the scope of the sigma
* instance.
*
* @param {?string} readPrefix The read prefix.
* @param {?string} writePrefix The write prefix.
* @param {object} options The parameters.
*/
sigma.middlewares.rescale = function(readPrefix, writePrefix, options) {
var i,
l,
a,
b,
c,
d,
scale,
margin,
n = this.graph.nodes(),
e = this.graph.edges(),
settings = this.settings.embedObjects(options || {}),
bounds = settings('bounds') || sigma.utils.getBoundaries(
this.graph,
readPrefix,
true
),
minX = bounds.minX,
minY = bounds.minY,
maxX = bounds.maxX,
maxY = bounds.maxY,
sizeMax = bounds.sizeMax,
weightMax = bounds.weightMax,
w = settings('width') || 1,
h = settings('height') || 1,
rescaleSettings = settings('autoRescale'),
validSettings = {
nodePosition: 1,
nodeSize: 1,
edgeSize: 1
};
/**
* What elements should we rescale?
*/
if (!(rescaleSettings instanceof Array))
rescaleSettings = ['nodePosition', 'nodeSize', 'edgeSize'];
for (i = 0, l = rescaleSettings.length; i < l; i++)
if (!validSettings[rescaleSettings[i]])
throw new Error(
'The rescale setting "' + rescaleSettings[i] + '" is not recognized.'
);
var np = ~rescaleSettings.indexOf('nodePosition'),
ns = ~rescaleSettings.indexOf('nodeSize'),
es = ~rescaleSettings.indexOf('edgeSize');
/**
* First, we compute the scaling ratio, without considering the sizes
* of the nodes : Each node will have its center in the canvas, but might
* be partially out of it.
*/
scale = settings('scalingMode') === 'outside' ?
Math.max(
w / Math.max(maxX - minX, 1),
h / Math.max(maxY - minY, 1)
) :
Math.min(
w / Math.max(maxX - minX, 1),
h / Math.max(maxY - minY, 1)
);
/**
* Then, we correct that scaling ratio considering a margin, which is
* basically the size of the biggest node.
* This has to be done as a correction since to compare the size of the
* biggest node to the X and Y values, we have to first get an
* approximation of the scaling ratio.
**/
margin =
(
settings('rescaleIgnoreSize') ?
0 :
(settings('maxNodeSize') || sizeMax) / scale
) +
(settings('sideMargin') || 0);
maxX += margin;
minX -= margin;
maxY += margin;
minY -= margin;
// Fix the scaling with the new extrema:
scale = settings('scalingMode') === 'outside' ?
Math.max(
w / Math.max(maxX - minX, 1),
h / Math.max(maxY - minY, 1)
) :
Math.min(
w / Math.max(maxX - minX, 1),
h / Math.max(maxY - minY, 1)
);
// Size homothetic parameters:
if (!settings('maxNodeSize') && !settings('minNodeSize')) {
a = 1;
b = 0;
} else if (settings('maxNodeSize') === settings('minNodeSize')) {
a = 0;
b = +settings('maxNodeSize');
} else {
a = (settings('maxNodeSize') - settings('minNodeSize')) / sizeMax;
b = +settings('minNodeSize');
}
if (!settings('maxEdgeSize') && !settings('minEdgeSize')) {
c = 1;
d = 0;
} else if (settings('maxEdgeSize') === settings('minEdgeSize')) {
c = 0;
d = +settings('minEdgeSize');
} else {
c = (settings('maxEdgeSize') - settings('minEdgeSize')) / weightMax;
d = +settings('minEdgeSize');
}
// Rescale the nodes and edges:
for (i = 0, l = e.length; i < l; i++)
e[i][writePrefix + 'size'] =
e[i][readPrefix + 'size'] * (es ? c : 1) + (es ? d : 0);
for (i = 0, l = n.length; i < l; i++) {
n[i][writePrefix + 'size'] =
n[i][readPrefix + 'size'] * (ns ? a : 1) + (ns ? b : 0);
n[i][writePrefix + 'x'] =
(n[i][readPrefix + 'x'] - (maxX + minX) / 2) * (np ? scale : 1);
n[i][writePrefix + 'y'] =
(n[i][readPrefix + 'y'] - (maxY + minY) / 2) * (np ? scale : 1);
}
};
sigma.utils.getBoundaries = function(graph, prefix, doEdges) {
var i,
l,
e = graph.edges(),
n = graph.nodes(),
weightMax = -Infinity,
sizeMax = -Infinity,
minX = Infinity,
minY = Infinity,
maxX = -Infinity,
maxY = -Infinity;
if (doEdges)
for (i = 0, l = e.length; i < l; i++)
weightMax = Math.max(e[i][prefix + 'size'], weightMax);
for (i = 0, l = n.length; i < l; i++) {
sizeMax = Math.max(n[i][prefix + 'size'], sizeMax);
maxX = Math.max(n[i][prefix + 'x'], maxX);
minX = Math.min(n[i][prefix + 'x'], minX);
maxY = Math.max(n[i][prefix + 'y'], maxY);
minY = Math.min(n[i][prefix + 'y'], minY);
}
weightMax = weightMax || 1;
sizeMax = sizeMax || 1;
return {
weightMax: weightMax,
sizeMax: sizeMax,
minX: minX,
minY: minY,
maxX: maxX,
maxY: maxY
};
};
}).call(this);

View File

@ -0,0 +1,239 @@
;(function(undefined) {
'use strict';
if (typeof sigma === 'undefined')
throw 'sigma is not declared';
// Initialize packages:
sigma.utils.pkg('sigma.misc.animation.running');
/**
* Generates a unique ID for the animation.
*
* @return {string} Returns the new ID.
*/
var _getID = (function() {
var id = 0;
return function() {
return '' + (++id);
};
})();
/**
* This function animates a camera. It has to be called with the camera to
* animate, the values of the coordinates to reach and eventually some
* options. It returns a number id, that you can use to kill the animation,
* with the method sigma.misc.animation.kill(id).
*
* The available options are:
*
* {?number} duration The duration of the animation.
* {?function} onNewFrame A callback to execute when the animation
* enter a new frame.
* {?function} onComplete A callback to execute when the animation
* is completed or killed.
* {?(string|function)} easing The name of a function from the package
* sigma.utils.easings, or a custom easing
* function.
*
* @param {camera} camera The camera to animate.
* @param {object} target The coordinates to reach.
* @param {?object} options Eventually an object to specify some options to
* the function. The available options are
* presented in the description of the function.
* @return {number} The animation id, to make it easy to kill
* through the method "sigma.misc.animation.kill".
*/
sigma.misc.animation.camera = function(camera, val, options) {
if (
!(camera instanceof sigma.classes.camera) ||
typeof val !== 'object' ||
!val
)
throw 'animation.camera: Wrong arguments.';
if (
typeof val.x !== 'number' &&
typeof val.y !== 'number' &&
typeof val.ratio !== 'number' &&
typeof val.angle !== 'number'
)
throw 'There must be at least one valid coordinate in the given val.';
var fn,
id,
anim,
easing,
duration,
initialVal,
o = options || {},
start = sigma.utils.dateNow();
// Store initial values:
initialVal = {
x: camera.x,
y: camera.y,
ratio: camera.ratio,
angle: camera.angle
};
duration = o.duration;
easing = typeof o.easing !== 'function' ?
sigma.utils.easings[o.easing || 'quadraticInOut'] :
o.easing;
fn = function() {
var coef,
t = o.duration ? (sigma.utils.dateNow() - start) / o.duration : 1;
// If the animation is over:
if (t >= 1) {
camera.isAnimated = false;
camera.goTo({
x: val.x !== undefined ? val.x : initialVal.x,
y: val.y !== undefined ? val.y : initialVal.y,
ratio: val.ratio !== undefined ? val.ratio : initialVal.ratio,
angle: val.angle !== undefined ? val.angle : initialVal.angle
});
cancelAnimationFrame(id);
delete sigma.misc.animation.running[id];
// Check callbacks:
if (typeof o.onComplete === 'function')
o.onComplete();
// Else, let's keep going:
} else {
coef = easing(t);
camera.isAnimated = true;
camera.goTo({
x: val.x !== undefined ?
initialVal.x + (val.x - initialVal.x) * coef :
initialVal.x,
y: val.y !== undefined ?
initialVal.y + (val.y - initialVal.y) * coef :
initialVal.y,
ratio: val.ratio !== undefined ?
initialVal.ratio + (val.ratio - initialVal.ratio) * coef :
initialVal.ratio,
angle: val.angle !== undefined ?
initialVal.angle + (val.angle - initialVal.angle) * coef :
initialVal.angle
});
// Check callbacks:
if (typeof o.onNewFrame === 'function')
o.onNewFrame();
anim.frameId = requestAnimationFrame(fn);
}
};
id = _getID();
anim = {
frameId: requestAnimationFrame(fn),
target: camera,
type: 'camera',
options: o,
fn: fn
};
sigma.misc.animation.running[id] = anim;
return id;
};
/**
* Kills a running animation. It triggers the eventual onComplete callback.
*
* @param {number} id The id of the animation to kill.
* @return {object} Returns the sigma.misc.animation package.
*/
sigma.misc.animation.kill = function(id) {
if (arguments.length !== 1 || typeof id !== 'number')
throw 'animation.kill: Wrong arguments.';
var o = sigma.misc.animation.running[id];
if (o) {
cancelAnimationFrame(id);
delete sigma.misc.animation.running[o.frameId];
if (o.type === 'camera')
o.target.isAnimated = false;
// Check callbacks:
if (typeof (o.options || {}).onComplete === 'function')
o.options.onComplete();
}
return this;
};
/**
* Kills every running animations, or only the one with the specified type,
* if a string parameter is given.
*
* @param {?(string|object)} filter A string to filter the animations to kill
* on their type (example: "camera"), or an
* object to filter on their target.
* @return {number} Returns the number of animations killed
* that way.
*/
sigma.misc.animation.killAll = function(filter) {
var o,
id,
count = 0,
type = typeof filter === 'string' ? filter : null,
target = typeof filter === 'object' ? filter : null,
running = sigma.misc.animation.running;
for (id in running)
if (
(!type || running[id].type === type) &&
(!target || running[id].target === target)
) {
o = sigma.misc.animation.running[id];
cancelAnimationFrame(o.frameId);
delete sigma.misc.animation.running[id];
if (o.type === 'camera')
o.target.isAnimated = false;
// Increment counter:
count++;
// Check callbacks:
if (typeof (o.options || {}).onComplete === 'function')
o.options.onComplete();
}
return count;
};
/**
* Returns "true" if any animation that is currently still running matches
* the filter given to the function.
*
* @param {string|object} filter A string to filter the animations to kill
* on their type (example: "camera"), or an
* object to filter on their target.
* @return {boolean} Returns true if any running animation
* matches.
*/
sigma.misc.animation.has = function(filter) {
var id,
type = typeof filter === 'string' ? filter : null,
target = typeof filter === 'object' ? filter : null,
running = sigma.misc.animation.running;
for (id in running)
if (
(!type || running[id].type === type) &&
(!target || running[id].target === target)
)
return true;
return false;
};
}).call(this);

View File

@ -0,0 +1,156 @@
;(function(undefined) {
'use strict';
if (typeof sigma === 'undefined')
throw 'sigma is not declared';
// Initialize packages:
sigma.utils.pkg('sigma.misc');
/**
* This helper will bind any DOM renderer (for instance svg)
* to its captors, to properly dispatch the good events to the sigma instance
* to manage clicking, hovering etc...
*
* It has to be called in the scope of the related renderer.
*/
sigma.misc.bindDOMEvents = function(container) {
var self = this,
graph = this.graph;
// DOMElement abstraction
function Element(domElement) {
// Helpers
this.attr = function(attrName) {
return domElement.getAttributeNS(null, attrName);
};
// Properties
this.tag = domElement.tagName;
this.class = this.attr('class');
this.id = this.attr('id');
// Methods
this.isNode = function() {
return !!~this.class.indexOf(self.settings('classPrefix') + '-node');
};
this.isEdge = function() {
return !!~this.class.indexOf(self.settings('classPrefix') + '-edge');
};
this.isHover = function() {
return !!~this.class.indexOf(self.settings('classPrefix') + '-hover');
};
}
// Click
function click(e) {
if (!self.settings('eventsEnabled'))
return;
// Generic event
self.dispatchEvent('click', e);
// Are we on a node?
var element = new Element(e.target);
if (element.isNode())
self.dispatchEvent('clickNode', {
node: graph.nodes(element.attr('data-node-id'))
});
else
self.dispatchEvent('clickStage');
e.preventDefault();
e.stopPropagation();
}
// Double click
function doubleClick(e) {
if (!self.settings('eventsEnabled'))
return;
// Generic event
self.dispatchEvent('doubleClick', e);
// Are we on a node?
var element = new Element(e.target);
if (element.isNode())
self.dispatchEvent('doubleClickNode', {
node: graph.nodes(element.attr('data-node-id'))
});
else
self.dispatchEvent('doubleClickStage');
e.preventDefault();
e.stopPropagation();
}
// On over
function onOver(e) {
var target = e.toElement || e.target;
if (!self.settings('eventsEnabled') || !target)
return;
var el = new Element(target);
if (el.isNode()) {
self.dispatchEvent('overNode', {
node: graph.nodes(el.attr('data-node-id'))
});
}
else if (el.isEdge()) {
var edge = graph.edges(el.attr('data-edge-id'));
self.dispatchEvent('overEdge', {
edge: edge,
source: graph.nodes(edge.source),
target: graph.nodes(edge.target)
});
}
}
// On out
function onOut(e) {
var target = e.fromElement || e.originalTarget;
if (!self.settings('eventsEnabled'))
return;
var el = new Element(target);
if (el.isNode()) {
self.dispatchEvent('outNode', {
node: graph.nodes(el.attr('data-node-id'))
});
}
else if (el.isEdge()) {
var edge = graph.edges(el.attr('data-edge-id'));
self.dispatchEvent('outEdge', {
edge: edge,
source: graph.nodes(edge.source),
target: graph.nodes(edge.target)
});
}
}
// Registering Events:
// Click
container.addEventListener('click', click, false);
sigma.utils.doubleClick(container, 'click', doubleClick);
// Touch counterparts
container.addEventListener('touchstart', click, false);
sigma.utils.doubleClick(container, 'touchstart', doubleClick);
// Mouseover
container.addEventListener('mouseover', onOver, true);
// Mouseout
container.addEventListener('mouseout', onOut, true);
};
}).call(this);

View File

@ -0,0 +1,509 @@
;(function(undefined) {
'use strict';
if (typeof sigma === 'undefined')
throw 'sigma is not declared';
// Initialize packages:
sigma.utils.pkg('sigma.misc');
/**
* This helper will bind any no-DOM renderer (for instance canvas or WebGL)
* to its captors, to properly dispatch the good events to the sigma instance
* to manage clicking, hovering etc...
*
* It has to be called in the scope of the related renderer.
*/
sigma.misc.bindEvents = function(prefix) {
var i,
l,
mX,
mY,
captor,
self = this;
function getNodes(e) {
if (e) {
mX = 'x' in e.data ? e.data.x : mX;
mY = 'y' in e.data ? e.data.y : mY;
}
var i,
j,
l,
n,
x,
y,
s,
inserted,
selected = [],
modifiedX = mX + self.width / 2,
modifiedY = mY + self.height / 2,
point = self.camera.cameraPosition(
mX,
mY
),
nodes = self.camera.quadtree.point(
point.x,
point.y
);
if (nodes.length)
for (i = 0, l = nodes.length; i < l; i++) {
n = nodes[i];
x = n[prefix + 'x'];
y = n[prefix + 'y'];
s = n[prefix + 'size'];
if (
!n.hidden &&
modifiedX > x - s &&
modifiedX < x + s &&
modifiedY > y - s &&
modifiedY < y + s &&
Math.sqrt(
Math.pow(modifiedX - x, 2) +
Math.pow(modifiedY - y, 2)
) < s
) {
// Insert the node:
inserted = false;
for (j = 0; j < selected.length; j++)
if (n.size > selected[j].size) {
selected.splice(j, 0, n);
inserted = true;
break;
}
if (!inserted)
selected.push(n);
}
}
return selected;
}
function getEdges(e) {
if (!self.settings('enableEdgeHovering')) {
// No event if the setting is off:
return [];
}
var isCanvas = (
sigma.renderers.canvas && self instanceof sigma.renderers.canvas);
if (!isCanvas) {
// A quick hardcoded rule to prevent people from using this feature
// with the WebGL renderer (which is not good enough at the moment):
throw new Error(
'The edge events feature is not compatible with the WebGL renderer'
);
}
if (e) {
mX = 'x' in e.data ? e.data.x : mX;
mY = 'y' in e.data ? e.data.y : mY;
}
var i,
j,
l,
a,
edge,
s,
maxEpsilon = self.settings('edgeHoverPrecision'),
source,
target,
cp,
nodeIndex = {},
inserted,
selected = [],
modifiedX = mX + self.width / 2,
modifiedY = mY + self.height / 2,
point = self.camera.cameraPosition(
mX,
mY
),
edges = [];
if (isCanvas) {
var nodesOnScreen = self.camera.quadtree.area(
self.camera.getRectangle(self.width, self.height)
);
for (a = nodesOnScreen, i = 0, l = a.length; i < l; i++)
nodeIndex[a[i].id] = a[i];
}
if (self.camera.edgequadtree !== undefined) {
edges = self.camera.edgequadtree.point(
point.x,
point.y
);
}
function insertEdge(selected, edge) {
inserted = false;
for (j = 0; j < selected.length; j++)
if (edge.size > selected[j].size) {
selected.splice(j, 0, edge);
inserted = true;
break;
}
if (!inserted)
selected.push(edge);
}
if (edges.length)
for (i = 0, l = edges.length; i < l; i++) {
edge = edges[i];
source = self.graph.nodes(edge.source);
target = self.graph.nodes(edge.target);
// (HACK) we can't get edge[prefix + 'size'] on WebGL renderer:
s = edge[prefix + 'size'] ||
edge['read_' + prefix + 'size'];
// First, let's identify which edges are drawn. To do this, we keep
// every edges that have at least one extremity displayed according to
// the quadtree and the "hidden" attribute. We also do not keep hidden
// edges.
// Then, let's check if the mouse is on the edge (we suppose that it
// is a line segment).
if (
!edge.hidden &&
!source.hidden && !target.hidden &&
(!isCanvas ||
(nodeIndex[edge.source] || nodeIndex[edge.target])) &&
sigma.utils.getDistance(
source[prefix + 'x'],
source[prefix + 'y'],
modifiedX,
modifiedY) > source[prefix + 'size'] &&
sigma.utils.getDistance(
target[prefix + 'x'],
target[prefix + 'y'],
modifiedX,
modifiedY) > target[prefix + 'size']
) {
if (edge.type == 'curve' || edge.type == 'curvedArrow') {
if (source.id === target.id) {
cp = sigma.utils.getSelfLoopControlPoints(
source[prefix + 'x'],
source[prefix + 'y'],
source[prefix + 'size']
);
if (
sigma.utils.isPointOnBezierCurve(
modifiedX,
modifiedY,
source[prefix + 'x'],
source[prefix + 'y'],
target[prefix + 'x'],
target[prefix + 'y'],
cp.x1,
cp.y1,
cp.x2,
cp.y2,
Math.max(s, maxEpsilon)
)) {
insertEdge(selected, edge);
}
}
else {
cp = sigma.utils.getQuadraticControlPoint(
source[prefix + 'x'],
source[prefix + 'y'],
target[prefix + 'x'],
target[prefix + 'y']);
if (
sigma.utils.isPointOnQuadraticCurve(
modifiedX,
modifiedY,
source[prefix + 'x'],
source[prefix + 'y'],
target[prefix + 'x'],
target[prefix + 'y'],
cp.x,
cp.y,
Math.max(s, maxEpsilon)
)) {
insertEdge(selected, edge);
}
}
} else if (
sigma.utils.isPointOnSegment(
modifiedX,
modifiedY,
source[prefix + 'x'],
source[prefix + 'y'],
target[prefix + 'x'],
target[prefix + 'y'],
Math.max(s, maxEpsilon)
)) {
insertEdge(selected, edge);
}
}
}
return selected;
}
function bindCaptor(captor) {
var nodes,
edges,
overNodes = {},
overEdges = {};
function onClick(e) {
if (!self.settings('eventsEnabled'))
return;
self.dispatchEvent('click', e.data);
nodes = getNodes(e);
edges = getEdges(e);
if (nodes.length) {
self.dispatchEvent('clickNode', {
node: nodes[0],
captor: e.data
});
self.dispatchEvent('clickNodes', {
node: nodes,
captor: e.data
});
} else if (edges.length) {
self.dispatchEvent('clickEdge', {
edge: edges[0],
captor: e.data
});
self.dispatchEvent('clickEdges', {
edge: edges,
captor: e.data
});
} else
self.dispatchEvent('clickStage', {captor: e.data});
}
function onDoubleClick(e) {
if (!self.settings('eventsEnabled'))
return;
self.dispatchEvent('doubleClick', e.data);
nodes = getNodes(e);
edges = getEdges(e);
if (nodes.length) {
self.dispatchEvent('doubleClickNode', {
node: nodes[0],
captor: e.data
});
self.dispatchEvent('doubleClickNodes', {
node: nodes,
captor: e.data
});
} else if (edges.length) {
self.dispatchEvent('doubleClickEdge', {
edge: edges[0],
captor: e.data
});
self.dispatchEvent('doubleClickEdges', {
edge: edges,
captor: e.data
});
} else
self.dispatchEvent('doubleClickStage', {captor: e.data});
}
function onRightClick(e) {
if (!self.settings('eventsEnabled'))
return;
self.dispatchEvent('rightClick', e.data);
nodes = getNodes(e);
edges = getEdges(e);
if (nodes.length) {
self.dispatchEvent('rightClickNode', {
node: nodes[0],
captor: e.data
});
self.dispatchEvent('rightClickNodes', {
node: nodes,
captor: e.data
});
} else if (edges.length) {
self.dispatchEvent('rightClickEdge', {
edge: edges[0],
captor: e.data
});
self.dispatchEvent('rightClickEdges', {
edge: edges,
captor: e.data
});
} else
self.dispatchEvent('rightClickStage', {captor: e.data});
}
function onOut(e) {
if (!self.settings('eventsEnabled'))
return;
var k,
i,
l,
le,
outNodes = [],
outEdges = [];
for (k in overNodes)
outNodes.push(overNodes[k]);
overNodes = {};
// Dispatch both single and multi events:
for (i = 0, l = outNodes.length; i < l; i++)
self.dispatchEvent('outNode', {
node: outNodes[i],
captor: e.data
});
if (outNodes.length)
self.dispatchEvent('outNodes', {
nodes: outNodes,
captor: e.data
});
overEdges = {};
// Dispatch both single and multi events:
for (i = 0, le = outEdges.length; i < le; i++)
self.dispatchEvent('outEdge', {
edge: outEdges[i],
captor: e.data
});
if (outEdges.length)
self.dispatchEvent('outEdges', {
edges: outEdges,
captor: e.data
});
}
function onMove(e) {
if (!self.settings('eventsEnabled'))
return;
nodes = getNodes(e);
edges = getEdges(e);
var i,
k,
node,
edge,
newOutNodes = [],
newOverNodes = [],
currentOverNodes = {},
l = nodes.length,
newOutEdges = [],
newOverEdges = [],
currentOverEdges = {},
le = edges.length;
// Check newly overred nodes:
for (i = 0; i < l; i++) {
node = nodes[i];
currentOverNodes[node.id] = node;
if (!overNodes[node.id]) {
newOverNodes.push(node);
overNodes[node.id] = node;
}
}
// Check no more overred nodes:
for (k in overNodes)
if (!currentOverNodes[k]) {
newOutNodes.push(overNodes[k]);
delete overNodes[k];
}
// Dispatch both single and multi events:
for (i = 0, l = newOverNodes.length; i < l; i++)
self.dispatchEvent('overNode', {
node: newOverNodes[i],
captor: e.data
});
for (i = 0, l = newOutNodes.length; i < l; i++)
self.dispatchEvent('outNode', {
node: newOutNodes[i],
captor: e.data
});
if (newOverNodes.length)
self.dispatchEvent('overNodes', {
nodes: newOverNodes,
captor: e.data
});
if (newOutNodes.length)
self.dispatchEvent('outNodes', {
nodes: newOutNodes,
captor: e.data
});
// Check newly overred edges:
for (i = 0; i < le; i++) {
edge = edges[i];
currentOverEdges[edge.id] = edge;
if (!overEdges[edge.id]) {
newOverEdges.push(edge);
overEdges[edge.id] = edge;
}
}
// Check no more overred edges:
for (k in overEdges)
if (!currentOverEdges[k]) {
newOutEdges.push(overEdges[k]);
delete overEdges[k];
}
// Dispatch both single and multi events:
for (i = 0, le = newOverEdges.length; i < le; i++)
self.dispatchEvent('overEdge', {
edge: newOverEdges[i],
captor: e.data
});
for (i = 0, le = newOutEdges.length; i < le; i++)
self.dispatchEvent('outEdge', {
edge: newOutEdges[i],
captor: e.data
});
if (newOverEdges.length)
self.dispatchEvent('overEdges', {
edges: newOverEdges,
captor: e.data
});
if (newOutEdges.length)
self.dispatchEvent('outEdges', {
edges: newOutEdges,
captor: e.data
});
}
// Bind events:
captor.bind('click', onClick);
captor.bind('mousedown', onMove);
captor.bind('mouseup', onMove);
captor.bind('mousemove', onMove);
captor.bind('mouseout', onOut);
captor.bind('doubleclick', onDoubleClick);
captor.bind('rightclick', onRightClick);
self.bind('render', onMove);
}
for (i = 0, l = this.captors.length; i < l; i++)
bindCaptor(this.captors[i]);
};
}).call(this);

View File

@ -0,0 +1,220 @@
;(function(undefined) {
'use strict';
if (typeof sigma === 'undefined')
throw 'sigma is not declared';
// Initialize packages:
sigma.utils.pkg('sigma.misc');
/**
* This method listens to "overNode", "outNode", "overEdge" and "outEdge"
* events from a renderer and renders the nodes differently on the top layer.
* The goal is to make any node label readable with the mouse, and to
* highlight hovered nodes and edges.
*
* It has to be called in the scope of the related renderer.
*/
sigma.misc.drawHovers = function(prefix) {
var self = this,
hoveredNodes = {},
hoveredEdges = {};
this.bind('overNode', function(event) {
var node = event.data.node;
if (!node.hidden) {
hoveredNodes[node.id] = node;
draw();
}
});
this.bind('outNode', function(event) {
delete hoveredNodes[event.data.node.id];
draw();
});
this.bind('overEdge', function(event) {
var edge = event.data.edge;
if (!edge.hidden) {
hoveredEdges[edge.id] = edge;
draw();
}
});
this.bind('outEdge', function(event) {
delete hoveredEdges[event.data.edge.id];
draw();
});
this.bind('render', function(event) {
draw();
});
function draw() {
// Clear self.contexts.hover:
self.contexts.hover.canvas.width = self.contexts.hover.canvas.width;
var k,
source,
target,
hoveredNode,
hoveredEdge,
defaultNodeType = self.settings('defaultNodeType'),
defaultEdgeType = self.settings('defaultEdgeType'),
nodeRenderers = sigma.canvas.hovers,
edgeRenderers = sigma.canvas.edgehovers,
extremitiesRenderers = sigma.canvas.extremities,
embedSettings = self.settings.embedObjects({
prefix: prefix
});
// Node render: single hover
if (
embedSettings('enableHovering') &&
embedSettings('singleHover') &&
Object.keys(hoveredNodes).length
) {
hoveredNode = hoveredNodes[Object.keys(hoveredNodes)[0]];
(
nodeRenderers[hoveredNode.type] ||
nodeRenderers[defaultNodeType] ||
nodeRenderers.def
)(
hoveredNode,
self.contexts.hover,
embedSettings
);
}
// Node render: multiple hover
if (
embedSettings('enableHovering') &&
!embedSettings('singleHover')
)
for (k in hoveredNodes)
(
nodeRenderers[hoveredNodes[k].type] ||
nodeRenderers[defaultNodeType] ||
nodeRenderers.def
)(
hoveredNodes[k],
self.contexts.hover,
embedSettings
);
// Edge render: single hover
if (
embedSettings('enableEdgeHovering') &&
embedSettings('singleHover') &&
Object.keys(hoveredEdges).length
) {
hoveredEdge = hoveredEdges[Object.keys(hoveredEdges)[0]];
source = self.graph.nodes(hoveredEdge.source);
target = self.graph.nodes(hoveredEdge.target);
if (! hoveredEdge.hidden) {
(
edgeRenderers[hoveredEdge.type] ||
edgeRenderers[defaultEdgeType] ||
edgeRenderers.def
) (
hoveredEdge,
source,
target,
self.contexts.hover,
embedSettings
);
if (embedSettings('edgeHoverExtremities')) {
(
extremitiesRenderers[hoveredEdge.type] ||
extremitiesRenderers.def
)(
hoveredEdge,
source,
target,
self.contexts.hover,
embedSettings
);
} else {
// Avoid edges rendered over nodes:
(
sigma.canvas.nodes[source.type] ||
sigma.canvas.nodes.def
) (
source,
self.contexts.hover,
embedSettings
);
(
sigma.canvas.nodes[target.type] ||
sigma.canvas.nodes.def
) (
target,
self.contexts.hover,
embedSettings
);
}
}
}
// Edge render: multiple hover
if (
embedSettings('enableEdgeHovering') &&
!embedSettings('singleHover')
) {
for (k in hoveredEdges) {
hoveredEdge = hoveredEdges[k];
source = self.graph.nodes(hoveredEdge.source);
target = self.graph.nodes(hoveredEdge.target);
if (!hoveredEdge.hidden) {
(
edgeRenderers[hoveredEdge.type] ||
edgeRenderers[defaultEdgeType] ||
edgeRenderers.def
) (
hoveredEdge,
source,
target,
self.contexts.hover,
embedSettings
);
if (embedSettings('edgeHoverExtremities')) {
(
extremitiesRenderers[hoveredEdge.type] ||
extremitiesRenderers.def
)(
hoveredEdge,
source,
target,
self.contexts.hover,
embedSettings
);
} else {
// Avoid edges rendered over nodes:
(
sigma.canvas.nodes[source.type] ||
sigma.canvas.nodes.def
) (
source,
self.contexts.hover,
embedSettings
);
(
sigma.canvas.nodes[target.type] ||
sigma.canvas.nodes.def
) (
target,
self.contexts.hover,
embedSettings
);
}
}
}
}
}
};
}).call(this);

View File

@ -0,0 +1,76 @@
;(function() {
'use strict';
sigma.utils.pkg('sigma.canvas.edgehovers');
/**
* This hover renderer will display the edge with a different color or size.
*
* @param {object} edge The edge object.
* @param {object} source node The edge source node.
* @param {object} target node The edge target node.
* @param {CanvasRenderingContext2D} context The canvas context.
* @param {configurable} settings The settings function.
*/
sigma.canvas.edgehovers.arrow =
function(edge, source, target, context, settings) {
var color = edge.color,
prefix = settings('prefix') || '',
edgeColor = settings('edgeColor'),
defaultNodeColor = settings('defaultNodeColor'),
defaultEdgeColor = settings('defaultEdgeColor'),
size = edge[prefix + 'size'] || 1,
tSize = target[prefix + 'size'],
sX = source[prefix + 'x'],
sY = source[prefix + 'y'],
tX = target[prefix + 'x'],
tY = target[prefix + 'y'];
size = (edge.hover) ?
settings('edgeHoverSizeRatio') * size : size;
var aSize = size * 2.5,
d = Math.sqrt(Math.pow(tX - sX, 2) + Math.pow(tY - sY, 2)),
aX = sX + (tX - sX) * (d - aSize - tSize) / d,
aY = sY + (tY - sY) * (d - aSize - tSize) / d,
vX = (tX - sX) * aSize / d,
vY = (tY - sY) * aSize / d;
if (!color)
switch (edgeColor) {
case 'source':
color = source.color || defaultNodeColor;
break;
case 'target':
color = target.color || defaultNodeColor;
break;
default:
color = defaultEdgeColor;
break;
}
if (settings('edgeHoverColor') === 'edge') {
color = edge.hover_color || color;
} else {
color = edge.hover_color || settings('defaultEdgeHoverColor') || color;
}
context.strokeStyle = color;
context.lineWidth = size;
context.beginPath();
context.moveTo(sX, sY);
context.lineTo(
aX,
aY
);
context.stroke();
context.fillStyle = color;
context.beginPath();
context.moveTo(aX + vX, aY + vY);
context.lineTo(aX + vY * 0.6, aY - vX * 0.6);
context.lineTo(aX - vY * 0.6, aY + vX * 0.6);
context.lineTo(aX + vX, aY + vY);
context.closePath();
context.fill();
};
})();

View File

@ -0,0 +1,64 @@
;(function() {
'use strict';
sigma.utils.pkg('sigma.canvas.edgehovers');
/**
* This hover renderer will display the edge with a different color or size.
*
* @param {object} edge The edge object.
* @param {object} source node The edge source node.
* @param {object} target node The edge target node.
* @param {CanvasRenderingContext2D} context The canvas context.
* @param {configurable} settings The settings function.
*/
sigma.canvas.edgehovers.curve =
function(edge, source, target, context, settings) {
var color = edge.color,
prefix = settings('prefix') || '',
size = settings('edgeHoverSizeRatio') * (edge[prefix + 'size'] || 1),
edgeColor = settings('edgeColor'),
defaultNodeColor = settings('defaultNodeColor'),
defaultEdgeColor = settings('defaultEdgeColor'),
cp = {},
sSize = source[prefix + 'size'],
sX = source[prefix + 'x'],
sY = source[prefix + 'y'],
tX = target[prefix + 'x'],
tY = target[prefix + 'y'];
cp = (source.id === target.id) ?
sigma.utils.getSelfLoopControlPoints(sX, sY, sSize) :
sigma.utils.getQuadraticControlPoint(sX, sY, tX, tY);
if (!color)
switch (edgeColor) {
case 'source':
color = source.color || defaultNodeColor;
break;
case 'target':
color = target.color || defaultNodeColor;
break;
default:
color = defaultEdgeColor;
break;
}
if (settings('edgeHoverColor') === 'edge') {
color = edge.hover_color || color;
} else {
color = edge.hover_color || settings('defaultEdgeHoverColor') || color;
}
context.strokeStyle = color;
context.lineWidth = size;
context.beginPath();
context.moveTo(sX, sY);
if (source.id === target.id) {
context.bezierCurveTo(cp.x1, cp.y1, cp.x2, cp.y2, tX, tY);
} else {
context.quadraticCurveTo(cp.x, cp.y, tX, tY);
}
context.stroke();
};
})();

View File

@ -0,0 +1,96 @@
;(function() {
'use strict';
sigma.utils.pkg('sigma.canvas.edgehovers');
/**
* This hover renderer will display the edge with a different color or size.
*
* @param {object} edge The edge object.
* @param {object} source node The edge source node.
* @param {object} target node The edge target node.
* @param {CanvasRenderingContext2D} context The canvas context.
* @param {configurable} settings The settings function.
*/
sigma.canvas.edgehovers.curvedArrow =
function(edge, source, target, context, settings) {
var color = edge.color,
prefix = settings('prefix') || '',
edgeColor = settings('edgeColor'),
defaultNodeColor = settings('defaultNodeColor'),
defaultEdgeColor = settings('defaultEdgeColor'),
cp = {},
size = settings('edgeHoverSizeRatio') * (edge[prefix + 'size'] || 1),
tSize = target[prefix + 'size'],
sX = source[prefix + 'x'],
sY = source[prefix + 'y'],
tX = target[prefix + 'x'],
tY = target[prefix + 'y'],
d,
aSize,
aX,
aY,
vX,
vY;
cp = (source.id === target.id) ?
sigma.utils.getSelfLoopControlPoints(sX, sY, tSize) :
sigma.utils.getQuadraticControlPoint(sX, sY, tX, tY);
if (source.id === target.id) {
d = Math.sqrt(Math.pow(tX - cp.x1, 2) + Math.pow(tY - cp.y1, 2));
aSize = size * 2.5;
aX = cp.x1 + (tX - cp.x1) * (d - aSize - tSize) / d;
aY = cp.y1 + (tY - cp.y1) * (d - aSize - tSize) / d;
vX = (tX - cp.x1) * aSize / d;
vY = (tY - cp.y1) * aSize / d;
}
else {
d = Math.sqrt(Math.pow(tX - cp.x, 2) + Math.pow(tY - cp.y, 2));
aSize = size * 2.5;
aX = cp.x + (tX - cp.x) * (d - aSize - tSize) / d;
aY = cp.y + (tY - cp.y) * (d - aSize - tSize) / d;
vX = (tX - cp.x) * aSize / d;
vY = (tY - cp.y) * aSize / d;
}
if (!color)
switch (edgeColor) {
case 'source':
color = source.color || defaultNodeColor;
break;
case 'target':
color = target.color || defaultNodeColor;
break;
default:
color = defaultEdgeColor;
break;
}
if (settings('edgeHoverColor') === 'edge') {
color = edge.hover_color || color;
} else {
color = edge.hover_color || settings('defaultEdgeHoverColor') || color;
}
context.strokeStyle = color;
context.lineWidth = size;
context.beginPath();
context.moveTo(sX, sY);
if (source.id === target.id) {
context.bezierCurveTo(cp.x2, cp.y2, cp.x1, cp.y1, aX, aY);
} else {
context.quadraticCurveTo(cp.x, cp.y, aX, aY);
}
context.stroke();
context.fillStyle = color;
context.beginPath();
context.moveTo(aX + vX, aY + vY);
context.lineTo(aX + vY * 0.6, aY - vX * 0.6);
context.lineTo(aX - vY * 0.6, aY + vX * 0.6);
context.lineTo(aX + vX, aY + vY);
context.closePath();
context.fill();
};
})();

View File

@ -0,0 +1,57 @@
;(function() {
'use strict';
sigma.utils.pkg('sigma.canvas.edgehovers');
/**
* This hover renderer will display the edge with a different color or size.
*
* @param {object} edge The edge object.
* @param {object} source node The edge source node.
* @param {object} target node The edge target node.
* @param {CanvasRenderingContext2D} context The canvas context.
* @param {configurable} settings The settings function.
*/
sigma.canvas.edgehovers.def =
function(edge, source, target, context, settings) {
var color = edge.color,
prefix = settings('prefix') || '',
size = edge[prefix + 'size'] || 1,
edgeColor = settings('edgeColor'),
defaultNodeColor = settings('defaultNodeColor'),
defaultEdgeColor = settings('defaultEdgeColor');
if (!color)
switch (edgeColor) {
case 'source':
color = source.color || defaultNodeColor;
break;
case 'target':
color = target.color || defaultNodeColor;
break;
default:
color = defaultEdgeColor;
break;
}
if (settings('edgeHoverColor') === 'edge') {
color = edge.hover_color || color;
} else {
color = edge.hover_color || settings('defaultEdgeHoverColor') || color;
}
size *= settings('edgeHoverSizeRatio');
context.strokeStyle = color;
context.lineWidth = size;
context.beginPath();
context.moveTo(
source[prefix + 'x'],
source[prefix + 'y']
);
context.lineTo(
target[prefix + 'x'],
target[prefix + 'y']
);
context.stroke();
};
})();

View File

@ -0,0 +1,66 @@
;(function() {
'use strict';
sigma.utils.pkg('sigma.canvas.edges');
/**
* This edge renderer will display edges as arrows going from the source node
*
* @param {object} edge The edge object.
* @param {object} source node The edge source node.
* @param {object} target node The edge target node.
* @param {CanvasRenderingContext2D} context The canvas context.
* @param {configurable} settings The settings function.
*/
sigma.canvas.edges.arrow = function(edge, source, target, context, settings) {
var color = edge.color,
prefix = settings('prefix') || '',
edgeColor = settings('edgeColor'),
defaultNodeColor = settings('defaultNodeColor'),
defaultEdgeColor = settings('defaultEdgeColor'),
size = edge[prefix + 'size'] || 1,
tSize = target[prefix + 'size'],
sX = source[prefix + 'x'],
sY = source[prefix + 'y'],
tX = target[prefix + 'x'],
tY = target[prefix + 'y'],
aSize = Math.max(size * 2.5, settings('minArrowSize')),
d = Math.sqrt(Math.pow(tX - sX, 2) + Math.pow(tY - sY, 2)),
aX = sX + (tX - sX) * (d - aSize - tSize) / d,
aY = sY + (tY - sY) * (d - aSize - tSize) / d,
vX = (tX - sX) * aSize / d,
vY = (tY - sY) * aSize / d;
if (!color)
switch (edgeColor) {
case 'source':
color = source.color || defaultNodeColor;
break;
case 'target':
color = target.color || defaultNodeColor;
break;
default:
color = defaultEdgeColor;
break;
}
context.strokeStyle = color;
context.lineWidth = size;
context.beginPath();
context.moveTo(sX, sY);
context.lineTo(
aX,
aY
);
context.stroke();
context.fillStyle = color;
context.beginPath();
context.moveTo(aX + vX, aY + vY);
context.lineTo(aX + vY * 0.6, aY - vX * 0.6);
context.lineTo(aX - vY * 0.6, aY + vX * 0.6);
context.lineTo(aX + vX, aY + vY);
context.closePath();
context.fill();
};
})();

View File

@ -0,0 +1,57 @@
;(function() {
'use strict';
sigma.utils.pkg('sigma.canvas.edges');
/**
* This edge renderer will display edges as curves.
*
* @param {object} edge The edge object.
* @param {object} source node The edge source node.
* @param {object} target node The edge target node.
* @param {CanvasRenderingContext2D} context The canvas context.
* @param {configurable} settings The settings function.
*/
sigma.canvas.edges.curve = function(edge, source, target, context, settings) {
var color = edge.color,
prefix = settings('prefix') || '',
size = edge[prefix + 'size'] || 1,
edgeColor = settings('edgeColor'),
defaultNodeColor = settings('defaultNodeColor'),
defaultEdgeColor = settings('defaultEdgeColor'),
cp = {},
sSize = source[prefix + 'size'],
sX = source[prefix + 'x'],
sY = source[prefix + 'y'],
tX = target[prefix + 'x'],
tY = target[prefix + 'y'];
cp = (source.id === target.id) ?
sigma.utils.getSelfLoopControlPoints(sX, sY, sSize) :
sigma.utils.getQuadraticControlPoint(sX, sY, tX, tY);
if (!color)
switch (edgeColor) {
case 'source':
color = source.color || defaultNodeColor;
break;
case 'target':
color = target.color || defaultNodeColor;
break;
default:
color = defaultEdgeColor;
break;
}
context.strokeStyle = color;
context.lineWidth = size;
context.beginPath();
context.moveTo(sX, sY);
if (source.id === target.id) {
context.bezierCurveTo(cp.x1, cp.y1, cp.x2, cp.y2, tX, tY);
} else {
context.quadraticCurveTo(cp.x, cp.y, tX, tY);
}
context.stroke();
};
})();

View File

@ -0,0 +1,88 @@
;(function() {
'use strict';
sigma.utils.pkg('sigma.canvas.edges');
/**
* This edge renderer will display edges as curves with arrow heading.
*
* @param {object} edge The edge object.
* @param {object} source node The edge source node.
* @param {object} target node The edge target node.
* @param {CanvasRenderingContext2D} context The canvas context.
* @param {configurable} settings The settings function.
*/
sigma.canvas.edges.curvedArrow =
function(edge, source, target, context, settings) {
var color = edge.color,
prefix = settings('prefix') || '',
edgeColor = settings('edgeColor'),
defaultNodeColor = settings('defaultNodeColor'),
defaultEdgeColor = settings('defaultEdgeColor'),
cp = {},
size = edge[prefix + 'size'] || 1,
tSize = target[prefix + 'size'],
sX = source[prefix + 'x'],
sY = source[prefix + 'y'],
tX = target[prefix + 'x'],
tY = target[prefix + 'y'],
aSize = Math.max(size * 2.5, settings('minArrowSize')),
d,
aX,
aY,
vX,
vY;
cp = (source.id === target.id) ?
sigma.utils.getSelfLoopControlPoints(sX, sY, tSize) :
sigma.utils.getQuadraticControlPoint(sX, sY, tX, tY);
if (source.id === target.id) {
d = Math.sqrt(Math.pow(tX - cp.x1, 2) + Math.pow(tY - cp.y1, 2));
aX = cp.x1 + (tX - cp.x1) * (d - aSize - tSize) / d;
aY = cp.y1 + (tY - cp.y1) * (d - aSize - tSize) / d;
vX = (tX - cp.x1) * aSize / d;
vY = (tY - cp.y1) * aSize / d;
}
else {
d = Math.sqrt(Math.pow(tX - cp.x, 2) + Math.pow(tY - cp.y, 2));
aX = cp.x + (tX - cp.x) * (d - aSize - tSize) / d;
aY = cp.y + (tY - cp.y) * (d - aSize - tSize) / d;
vX = (tX - cp.x) * aSize / d;
vY = (tY - cp.y) * aSize / d;
}
if (!color)
switch (edgeColor) {
case 'source':
color = source.color || defaultNodeColor;
break;
case 'target':
color = target.color || defaultNodeColor;
break;
default:
color = defaultEdgeColor;
break;
}
context.strokeStyle = color;
context.lineWidth = size;
context.beginPath();
context.moveTo(sX, sY);
if (source.id === target.id) {
context.bezierCurveTo(cp.x2, cp.y2, cp.x1, cp.y1, aX, aY);
} else {
context.quadraticCurveTo(cp.x, cp.y, aX, aY);
}
context.stroke();
context.fillStyle = color;
context.beginPath();
context.moveTo(aX + vX, aY + vY);
context.lineTo(aX + vY * 0.6, aY - vX * 0.6);
context.lineTo(aX - vY * 0.6, aY + vX * 0.6);
context.lineTo(aX + vX, aY + vY);
context.closePath();
context.fill();
};
})();

View File

@ -0,0 +1,49 @@
;(function() {
'use strict';
sigma.utils.pkg('sigma.canvas.edges');
/**
* The default edge renderer. It renders the edge as a simple line.
*
* @param {object} edge The edge object.
* @param {object} source node The edge source node.
* @param {object} target node The edge target node.
* @param {CanvasRenderingContext2D} context The canvas context.
* @param {configurable} settings The settings function.
*/
sigma.canvas.edges.def = function(edge, source, target, context, settings) {
var color = edge.color,
prefix = settings('prefix') || '',
size = edge[prefix + 'size'] || 1,
edgeColor = settings('edgeColor'),
defaultNodeColor = settings('defaultNodeColor'),
defaultEdgeColor = settings('defaultEdgeColor');
if (!color)
switch (edgeColor) {
case 'source':
color = source.color || defaultNodeColor;
break;
case 'target':
color = target.color || defaultNodeColor;
break;
default:
color = defaultEdgeColor;
break;
}
context.strokeStyle = color;
context.lineWidth = size;
context.beginPath();
context.moveTo(
source[prefix + 'x'],
source[prefix + 'y']
);
context.lineTo(
target[prefix + 'x'],
target[prefix + 'y']
);
context.stroke();
};
})();

View File

@ -0,0 +1,38 @@
;(function(undefined) {
'use strict';
if (typeof sigma === 'undefined')
throw 'sigma is not declared';
// Initialize packages:
sigma.utils.pkg('sigma.canvas.extremities');
/**
* The default renderer for hovered edge extremities. It renders the edge
* extremities as hovered.
*
* @param {object} edge The edge object.
* @param {object} source node The edge source node.
* @param {object} target node The edge target node.
* @param {CanvasRenderingContext2D} context The canvas context.
* @param {configurable} settings The settings function.
*/
sigma.canvas.extremities.def =
function(edge, source, target, context, settings) {
// Source Node:
(
sigma.canvas.hovers[source.type] ||
sigma.canvas.hovers.def
) (
source, context, settings
);
// Target Node:
(
sigma.canvas.hovers[target.type] ||
sigma.canvas.hovers.def
) (
target, context, settings
);
};
}).call(this);

View File

@ -0,0 +1,106 @@
;(function(undefined) {
'use strict';
if (typeof sigma === 'undefined')
throw 'sigma is not declared';
// Initialize packages:
sigma.utils.pkg('sigma.canvas.hovers');
/**
* This hover renderer will basically display the label with a background.
*
* @param {object} node The node object.
* @param {CanvasRenderingContext2D} context The canvas context.
* @param {configurable} settings The settings function.
*/
sigma.canvas.hovers.def = function(node, context, settings) {
var x,
y,
w,
h,
e,
fontStyle = settings('hoverFontStyle') || settings('fontStyle'),
prefix = settings('prefix') || '',
size = node[prefix + 'size'],
fontSize = (settings('labelSize') === 'fixed') ?
settings('defaultLabelSize') :
settings('labelSizeRatio') * size;
// Label background:
context.font = (fontStyle ? fontStyle + ' ' : '') +
fontSize + 'px ' + (settings('hoverFont') || settings('font'));
context.beginPath();
context.fillStyle = settings('labelHoverBGColor') === 'node' ?
(node.color || settings('defaultNodeColor')) :
settings('defaultHoverLabelBGColor');
if (node.label && settings('labelHoverShadow')) {
context.shadowOffsetX = 0;
context.shadowOffsetY = 0;
context.shadowBlur = 8;
context.shadowColor = settings('labelHoverShadowColor');
}
if (node.label && typeof node.label === 'string') {
x = Math.round(node[prefix + 'x'] - fontSize / 2 - 2);
y = Math.round(node[prefix + 'y'] - fontSize / 2 - 2);
w = Math.round(
context.measureText(node.label).width + fontSize / 2 + size + 7
);
h = Math.round(fontSize + 4);
e = Math.round(fontSize / 2 + 2);
context.moveTo(x, y + e);
context.arcTo(x, y, x + e, y, e);
context.lineTo(x + w, y);
context.lineTo(x + w, y + h);
context.lineTo(x + e, y + h);
context.arcTo(x, y + h, x, y + h - e, e);
context.lineTo(x, y + e);
context.closePath();
context.fill();
context.shadowOffsetX = 0;
context.shadowOffsetY = 0;
context.shadowBlur = 0;
}
// Node border:
if (settings('borderSize') > 0) {
context.beginPath();
context.fillStyle = settings('nodeBorderColor') === 'node' ?
(node.color || settings('defaultNodeColor')) :
settings('defaultNodeBorderColor');
context.arc(
node[prefix + 'x'],
node[prefix + 'y'],
size + settings('borderSize'),
0,
Math.PI * 2,
true
);
context.closePath();
context.fill();
}
// Node:
var nodeRenderer = sigma.canvas.nodes[node.type] || sigma.canvas.nodes.def;
nodeRenderer(node, context, settings);
// Display the label:
if (node.label && typeof node.label === 'string') {
context.fillStyle = (settings('labelHoverColor') === 'node') ?
(node.color || settings('defaultNodeColor')) :
settings('defaultLabelHoverColor');
context.fillText(
node.label,
Math.round(node[prefix + 'x'] + size + 3),
Math.round(node[prefix + 'y'] + fontSize / 3)
);
}
};
}).call(this);

View File

@ -0,0 +1,44 @@
;(function(undefined) {
'use strict';
if (typeof sigma === 'undefined')
throw 'sigma is not declared';
// Initialize packages:
sigma.utils.pkg('sigma.canvas.labels');
/**
* This label renderer will just display the label on the right of the node.
*
* @param {object} node The node object.
* @param {CanvasRenderingContext2D} context The canvas context.
* @param {configurable} settings The settings function.
*/
sigma.canvas.labels.def = function(node, context, settings) {
var fontSize,
prefix = settings('prefix') || '',
size = node[prefix + 'size'];
if (size < settings('labelThreshold'))
return;
if (!node.label || typeof node.label !== 'string')
return;
fontSize = (settings('labelSize') === 'fixed') ?
settings('defaultLabelSize') :
settings('labelSizeRatio') * size;
context.font = (settings('fontStyle') ? settings('fontStyle') + ' ' : '') +
fontSize + 'px ' + settings('font');
context.fillStyle = (settings('labelColor') === 'node') ?
(node.color || settings('defaultNodeColor')) :
settings('defaultLabelColor');
context.fillText(
node.label,
Math.round(node[prefix + 'x'] + size + 3),
Math.round(node[prefix + 'y'] + fontSize / 3)
);
};
}).call(this);

View File

@ -0,0 +1,30 @@
;(function() {
'use strict';
sigma.utils.pkg('sigma.canvas.nodes');
/**
* The default node renderer. It renders the node as a simple disc.
*
* @param {object} node The node object.
* @param {CanvasRenderingContext2D} context The canvas context.
* @param {configurable} settings The settings function.
*/
sigma.canvas.nodes.def = function(node, context, settings) {
var prefix = settings('prefix') || '';
context.fillStyle = node.color || settings('defaultNodeColor');
context.beginPath();
context.arc(
node[prefix + 'x'],
node[prefix + 'y'],
node[prefix + 'size'],
0,
Math.PI * 2,
true
);
context.closePath();
context.fill();
};
})();

View File

@ -0,0 +1,450 @@
;(function(undefined) {
'use strict';
if (typeof sigma === 'undefined')
throw 'sigma is not declared';
if (typeof conrad === 'undefined')
throw 'conrad is not declared';
// Initialize packages:
sigma.utils.pkg('sigma.renderers');
/**
* This function is the constructor of the canvas sigma's renderer.
*
* @param {sigma.classes.graph} graph The graph to render.
* @param {sigma.classes.camera} camera The camera.
* @param {configurable} settings The sigma instance settings
* function.
* @param {object} object The options object.
* @return {sigma.renderers.canvas} The renderer instance.
*/
sigma.renderers.canvas = function(graph, camera, settings, options) {
if (typeof options !== 'object')
throw 'sigma.renderers.canvas: Wrong arguments.';
if (!(options.container instanceof HTMLElement))
throw 'Container not found.';
var k,
i,
l,
a,
fn,
self = this;
sigma.classes.dispatcher.extend(this);
// Initialize main attributes:
Object.defineProperty(this, 'conradId', {
value: sigma.utils.id()
});
this.graph = graph;
this.camera = camera;
this.contexts = {};
this.domElements = {};
this.options = options;
this.container = this.options.container;
this.settings = (
typeof options.settings === 'object' &&
options.settings
) ?
settings.embedObjects(options.settings) :
settings;
// Node indexes:
this.nodesOnScreen = [];
this.edgesOnScreen = [];
// Conrad related attributes:
this.jobs = {};
// Find the prefix:
this.options.prefix = 'renderer' + this.conradId + ':';
// Initialize the DOM elements:
if (
!this.settings('batchEdgesDrawing')
) {
this.initDOM('canvas', 'scene');
this.contexts.edges = this.contexts.scene;
this.contexts.nodes = this.contexts.scene;
this.contexts.labels = this.contexts.scene;
} else {
this.initDOM('canvas', 'edges');
this.initDOM('canvas', 'scene');
this.contexts.nodes = this.contexts.scene;
this.contexts.labels = this.contexts.scene;
}
this.initDOM('canvas', 'mouse');
this.contexts.hover = this.contexts.mouse;
// Initialize captors:
this.captors = [];
a = this.options.captors || [sigma.captors.mouse, sigma.captors.touch];
for (i = 0, l = a.length; i < l; i++) {
fn = typeof a[i] === 'function' ? a[i] : sigma.captors[a[i]];
this.captors.push(
new fn(
this.domElements.mouse,
this.camera,
this.settings
)
);
}
// Deal with sigma events:
sigma.misc.bindEvents.call(this, this.options.prefix);
sigma.misc.drawHovers.call(this, this.options.prefix);
this.resize(false);
};
/**
* This method renders the graph on the canvases.
*
* @param {?object} options Eventually an object of options.
* @return {sigma.renderers.canvas} Returns the instance itself.
*/
sigma.renderers.canvas.prototype.render = function(options) {
options = options || {};
var a,
i,
k,
l,
o,
id,
end,
job,
start,
edges,
renderers,
rendererType,
batchSize,
tempGCO,
index = {},
graph = this.graph,
nodes = this.graph.nodes,
prefix = this.options.prefix || '',
drawEdges = this.settings(options, 'drawEdges'),
drawNodes = this.settings(options, 'drawNodes'),
drawLabels = this.settings(options, 'drawLabels'),
drawEdgeLabels = this.settings(options, 'drawEdgeLabels'),
embedSettings = this.settings.embedObjects(options, {
prefix: this.options.prefix
});
// Call the resize function:
this.resize(false);
// Check the 'hideEdgesOnMove' setting:
if (this.settings(options, 'hideEdgesOnMove'))
if (this.camera.isAnimated || this.camera.isMoving)
drawEdges = false;
// Apply the camera's view:
this.camera.applyView(
undefined,
this.options.prefix,
{
width: this.width,
height: this.height
}
);
// Clear canvases:
this.clear();
// Kill running jobs:
for (k in this.jobs)
if (conrad.hasJob(k))
conrad.killJob(k);
// Find which nodes are on screen:
this.edgesOnScreen = [];
this.nodesOnScreen = this.camera.quadtree.area(
this.camera.getRectangle(this.width, this.height)
);
for (a = this.nodesOnScreen, i = 0, l = a.length; i < l; i++)
index[a[i].id] = a[i];
// Draw edges:
// - If settings('batchEdgesDrawing') is true, the edges are displayed per
// batches. If not, they are drawn in one frame.
if (drawEdges) {
// First, let's identify which edges to draw. To do this, we just keep
// every edges that have at least one extremity displayed according to
// the quadtree and the "hidden" attribute. We also do not keep hidden
// edges.
for (a = graph.edges(), i = 0, l = a.length; i < l; i++) {
o = a[i];
if (
(index[o.source] || index[o.target]) &&
(!o.hidden && !nodes(o.source).hidden && !nodes(o.target).hidden)
)
this.edgesOnScreen.push(o);
}
// If the "batchEdgesDrawing" settings is true, edges are batched:
if (this.settings(options, 'batchEdgesDrawing')) {
id = 'edges_' + this.conradId;
batchSize = embedSettings('canvasEdgesBatchSize');
edges = this.edgesOnScreen;
l = edges.length;
start = 0;
end = Math.min(edges.length, start + batchSize);
job = function() {
tempGCO = this.contexts.edges.globalCompositeOperation;
this.contexts.edges.globalCompositeOperation = 'destination-over';
renderers = sigma.canvas.edges;
for (i = start; i < end; i++) {
o = edges[i];
(renderers[
o.type || this.settings(options, 'defaultEdgeType')
] || renderers.def)(
o,
graph.nodes(o.source),
graph.nodes(o.target),
this.contexts.edges,
embedSettings
);
}
// Draw edge labels:
if (drawEdgeLabels) {
renderers = sigma.canvas.edges.labels;
for (i = start; i < end; i++) {
o = edges[i];
if (!o.hidden)
(renderers[
o.type || this.settings(options, 'defaultEdgeType')
] || renderers.def)(
o,
graph.nodes(o.source),
graph.nodes(o.target),
this.contexts.labels,
embedSettings
);
}
}
// Restore original globalCompositeOperation:
this.contexts.edges.globalCompositeOperation = tempGCO;
// Catch job's end:
if (end === edges.length) {
delete this.jobs[id];
return false;
}
start = end + 1;
end = Math.min(edges.length, start + batchSize);
return true;
};
this.jobs[id] = job;
conrad.addJob(id, job.bind(this));
// If not, they are drawn in one frame:
} else {
renderers = sigma.canvas.edges;
for (a = this.edgesOnScreen, i = 0, l = a.length; i < l; i++) {
o = a[i];
(renderers[
o.type || this.settings(options, 'defaultEdgeType')
] || renderers.def)(
o,
graph.nodes(o.source),
graph.nodes(o.target),
this.contexts.edges,
embedSettings
);
}
// Draw edge labels:
// - No batching
if (drawEdgeLabels) {
renderers = sigma.canvas.edges.labels;
for (a = this.edgesOnScreen, i = 0, l = a.length; i < l; i++)
if (!a[i].hidden)
(renderers[
a[i].type || this.settings(options, 'defaultEdgeType')
] || renderers.def)(
a[i],
graph.nodes(a[i].source),
graph.nodes(a[i].target),
this.contexts.labels,
embedSettings
);
}
}
}
// Draw nodes:
// - No batching
if (drawNodes) {
renderers = sigma.canvas.nodes;
for (a = this.nodesOnScreen, i = 0, l = a.length; i < l; i++)
if (!a[i].hidden)
(renderers[
a[i].type || this.settings(options, 'defaultNodeType')
] || renderers.def)(
a[i],
this.contexts.nodes,
embedSettings
);
}
// Draw labels:
// - No batching
if (drawLabels) {
renderers = sigma.canvas.labels;
for (a = this.nodesOnScreen, i = 0, l = a.length; i < l; i++)
if (!a[i].hidden)
(renderers[
a[i].type || this.settings(options, 'defaultNodeType')
] || renderers.def)(
a[i],
this.contexts.labels,
embedSettings
);
}
this.dispatchEvent('render');
return this;
};
/**
* This method creates a DOM element of the specified type, switches its
* position to "absolute", references it to the domElements attribute, and
* finally appends it to the container.
*
* @param {string} tag The label tag.
* @param {string} id The id of the element (to store it in "domElements").
*/
sigma.renderers.canvas.prototype.initDOM = function(tag, id) {
var dom = document.createElement(tag);
dom.style.position = 'absolute';
dom.setAttribute('class', 'sigma-' + id);
this.domElements[id] = dom;
this.container.appendChild(dom);
if (tag.toLowerCase() === 'canvas')
this.contexts[id] = dom.getContext('2d');
};
/**
* This method resizes each DOM elements in the container and stores the new
* dimensions. Then, it renders the graph.
*
* @param {?number} width The new width of the container.
* @param {?number} height The new height of the container.
* @return {sigma.renderers.canvas} Returns the instance itself.
*/
sigma.renderers.canvas.prototype.resize = function(w, h) {
var k,
oldWidth = this.width,
oldHeight = this.height,
pixelRatio = 1;
// TODO:
// *****
// This pixelRatio is the solution to display with the good definition
// on canvases on Retina displays (ie oversampling). Unfortunately, it
// has a huge performance cost...
// > pixelRatio = window.devicePixelRatio || 1;
if (w !== undefined && h !== undefined) {
this.width = w;
this.height = h;
} else {
this.width = this.container.offsetWidth;
this.height = this.container.offsetHeight;
w = this.width;
h = this.height;
}
if (oldWidth !== this.width || oldHeight !== this.height) {
for (k in this.domElements) {
this.domElements[k].style.width = w + 'px';
this.domElements[k].style.height = h + 'px';
if (this.domElements[k].tagName.toLowerCase() === 'canvas') {
this.domElements[k].setAttribute('width', (w * pixelRatio) + 'px');
this.domElements[k].setAttribute('height', (h * pixelRatio) + 'px');
if (pixelRatio !== 1)
this.contexts[k].scale(pixelRatio, pixelRatio);
}
}
}
return this;
};
/**
* This method clears each canvas.
*
* @return {sigma.renderers.canvas} Returns the instance itself.
*/
sigma.renderers.canvas.prototype.clear = function() {
var k;
for (k in this.domElements)
if (this.domElements[k].tagName === 'CANVAS')
this.domElements[k].width = this.domElements[k].width;
return this;
};
/**
* This method kills contexts and other attributes.
*/
sigma.renderers.canvas.prototype.kill = function() {
var k,
captor;
// Kill captors:
while ((captor = this.captors.pop()))
captor.kill();
delete this.captors;
// Kill contexts:
for (k in this.domElements) {
this.domElements[k].parentNode.removeChild(this.domElements[k]);
delete this.domElements[k];
delete this.contexts[k];
}
delete this.domElements;
delete this.contexts;
};
/**
* The labels, nodes and edges renderers are stored in the three following
* objects. When an element is drawn, its type will be checked and if a
* renderer with the same name exists, it will be used. If not found, the
* default renderer will be used instead.
*
* They are stored in different files, in the "./canvas" folder.
*/
sigma.utils.pkg('sigma.canvas.nodes');
sigma.utils.pkg('sigma.canvas.edges');
sigma.utils.pkg('sigma.canvas.labels');
}).call(this);

View File

@ -0,0 +1,29 @@
;(function(global) {
'use strict';
if (typeof sigma === 'undefined')
throw 'sigma is not declared';
// Initialize packages:
sigma.utils.pkg('sigma.renderers');
// Check if WebGL is enabled:
var canvas,
webgl = !!global.WebGLRenderingContext;
if (webgl) {
canvas = document.createElement('canvas');
try {
webgl = !!(
canvas.getContext('webgl') ||
canvas.getContext('experimental-webgl')
);
} catch (e) {
webgl = false;
}
}
// Copy the good renderer:
sigma.renderers.def = webgl ?
sigma.renderers.webgl :
sigma.renderers.canvas;
})(this);

View File

@ -0,0 +1,479 @@
;(function(undefined) {
'use strict';
if (typeof sigma === 'undefined')
throw 'sigma is not declared';
if (typeof conrad === 'undefined')
throw 'conrad is not declared';
// Initialize packages:
sigma.utils.pkg('sigma.renderers');
/**
* This function is the constructor of the svg sigma's renderer.
*
* @param {sigma.classes.graph} graph The graph to render.
* @param {sigma.classes.camera} camera The camera.
* @param {configurable} settings The sigma instance settings
* function.
* @param {object} object The options object.
* @return {sigma.renderers.svg} The renderer instance.
*/
sigma.renderers.svg = function(graph, camera, settings, options) {
if (typeof options !== 'object')
throw 'sigma.renderers.svg: Wrong arguments.';
if (!(options.container instanceof HTMLElement))
throw 'Container not found.';
var i,
l,
a,
fn,
self = this;
sigma.classes.dispatcher.extend(this);
// Initialize main attributes:
this.graph = graph;
this.camera = camera;
this.domElements = {
graph: null,
groups: {},
nodes: {},
edges: {},
labels: {},
hovers: {}
};
this.measurementCanvas = null;
this.options = options;
this.container = this.options.container;
this.settings = (
typeof options.settings === 'object' &&
options.settings
) ?
settings.embedObjects(options.settings) :
settings;
// Is the renderer meant to be freestyle?
this.settings('freeStyle', !!this.options.freeStyle);
// SVG xmlns
this.settings('xmlns', 'http://www.w3.org/2000/svg');
// Indexes:
this.nodesOnScreen = [];
this.edgesOnScreen = [];
// Find the prefix:
this.options.prefix = 'renderer' + sigma.utils.id() + ':';
// Initialize the DOM elements
this.initDOM('svg');
// Initialize captors:
this.captors = [];
a = this.options.captors || [sigma.captors.mouse, sigma.captors.touch];
for (i = 0, l = a.length; i < l; i++) {
fn = typeof a[i] === 'function' ? a[i] : sigma.captors[a[i]];
this.captors.push(
new fn(
this.domElements.graph,
this.camera,
this.settings
)
);
}
// Bind resize:
window.addEventListener('resize', function() {
self.resize();
});
// Deal with sigma events:
// TODO: keep an option to override the DOM events?
sigma.misc.bindDOMEvents.call(this, this.domElements.graph);
this.bindHovers(this.options.prefix);
// Resize
this.resize(false);
};
/**
* This method renders the graph on the svg scene.
*
* @param {?object} options Eventually an object of options.
* @return {sigma.renderers.svg} Returns the instance itself.
*/
sigma.renderers.svg.prototype.render = function(options) {
options = options || {};
var a,
i,
k,
e,
l,
o,
source,
target,
start,
edges,
renderers,
subrenderers,
index = {},
graph = this.graph,
nodes = this.graph.nodes,
prefix = this.options.prefix || '',
drawEdges = this.settings(options, 'drawEdges'),
drawNodes = this.settings(options, 'drawNodes'),
drawLabels = this.settings(options, 'drawLabels'),
embedSettings = this.settings.embedObjects(options, {
prefix: this.options.prefix,
forceLabels: this.options.forceLabels
});
// Check the 'hideEdgesOnMove' setting:
if (this.settings(options, 'hideEdgesOnMove'))
if (this.camera.isAnimated || this.camera.isMoving)
drawEdges = false;
// Apply the camera's view:
this.camera.applyView(
undefined,
this.options.prefix,
{
width: this.width,
height: this.height
}
);
// Hiding everything
// TODO: find a more sensible way to perform this operation
this.hideDOMElements(this.domElements.nodes);
this.hideDOMElements(this.domElements.edges);
this.hideDOMElements(this.domElements.labels);
// Find which nodes are on screen
this.edgesOnScreen = [];
this.nodesOnScreen = this.camera.quadtree.area(
this.camera.getRectangle(this.width, this.height)
);
// Node index
for (a = this.nodesOnScreen, i = 0, l = a.length; i < l; i++)
index[a[i].id] = a[i];
// Find which edges are on screen
for (a = graph.edges(), i = 0, l = a.length; i < l; i++) {
o = a[i];
if (
(index[o.source] || index[o.target]) &&
(!o.hidden && !nodes(o.source).hidden && !nodes(o.target).hidden)
)
this.edgesOnScreen.push(o);
}
// Display nodes
//---------------
renderers = sigma.svg.nodes;
subrenderers = sigma.svg.labels;
//-- First we create the nodes which are not already created
if (drawNodes)
for (a = this.nodesOnScreen, i = 0, l = a.length; i < l; i++) {
if (!a[i].hidden && !this.domElements.nodes[a[i].id]) {
// Node
e = (renderers[a[i].type] || renderers.def).create(
a[i],
embedSettings
);
this.domElements.nodes[a[i].id] = e;
this.domElements.groups.nodes.appendChild(e);
// Label
e = (subrenderers[a[i].type] || subrenderers.def).create(
a[i],
embedSettings
);
this.domElements.labels[a[i].id] = e;
this.domElements.groups.labels.appendChild(e);
}
}
//-- Second we update the nodes
if (drawNodes)
for (a = this.nodesOnScreen, i = 0, l = a.length; i < l; i++) {
if (a[i].hidden)
continue;
// Node
(renderers[a[i].type] || renderers.def).update(
a[i],
this.domElements.nodes[a[i].id],
embedSettings
);
// Label
(subrenderers[a[i].type] || subrenderers.def).update(
a[i],
this.domElements.labels[a[i].id],
embedSettings
);
}
// Display edges
//---------------
renderers = sigma.svg.edges;
//-- First we create the edges which are not already created
if (drawEdges)
for (a = this.edgesOnScreen, i = 0, l = a.length; i < l; i++) {
if (!this.domElements.edges[a[i].id]) {
source = nodes(a[i].source);
target = nodes(a[i].target);
e = (renderers[a[i].type] || renderers.def).create(
a[i],
source,
target,
embedSettings
);
this.domElements.edges[a[i].id] = e;
this.domElements.groups.edges.appendChild(e);
}
}
//-- Second we update the edges
if (drawEdges)
for (a = this.edgesOnScreen, i = 0, l = a.length; i < l; i++) {
source = nodes(a[i].source);
target = nodes(a[i].target);
(renderers[a[i].type] || renderers.def).update(
a[i],
this.domElements.edges[a[i].id],
source,
target,
embedSettings
);
}
this.dispatchEvent('render');
return this;
};
/**
* This method creates a DOM element of the specified type, switches its
* position to "absolute", references it to the domElements attribute, and
* finally appends it to the container.
*
* @param {string} tag The label tag.
* @param {string} id The id of the element (to store it in "domElements").
*/
sigma.renderers.svg.prototype.initDOM = function(tag) {
var dom = document.createElementNS(this.settings('xmlns'), tag),
c = this.settings('classPrefix'),
g,
l,
i;
dom.style.position = 'absolute';
dom.setAttribute('class', c + '-svg');
// Setting SVG namespace
dom.setAttribute('xmlns', this.settings('xmlns'));
dom.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
dom.setAttribute('version', '1.1');
// Creating the measurement canvas
var canvas = document.createElement('canvas');
canvas.setAttribute('class', c + '-measurement-canvas');
// Appending elements
this.domElements.graph = this.container.appendChild(dom);
// Creating groups
var groups = ['edges', 'nodes', 'labels', 'hovers'];
for (i = 0, l = groups.length; i < l; i++) {
g = document.createElementNS(this.settings('xmlns'), 'g');
g.setAttributeNS(null, 'id', c + '-group-' + groups[i]);
g.setAttributeNS(null, 'class', c + '-group');
this.domElements.groups[groups[i]] =
this.domElements.graph.appendChild(g);
}
// Appending measurement canvas
this.container.appendChild(canvas);
this.measurementCanvas = canvas.getContext('2d');
};
/**
* This method hides a batch of SVG DOM elements.
*
* @param {array} elements An array of elements to hide.
* @param {object} renderer The renderer to use.
* @return {sigma.renderers.svg} Returns the instance itself.
*/
sigma.renderers.svg.prototype.hideDOMElements = function(elements) {
var o,
i;
for (i in elements) {
o = elements[i];
sigma.svg.utils.hide(o);
}
return this;
};
/**
* This method binds the hover events to the renderer.
*
* @param {string} prefix The renderer prefix.
*/
// TODO: add option about whether to display hovers or not
sigma.renderers.svg.prototype.bindHovers = function(prefix) {
var renderers = sigma.svg.hovers,
self = this,
hoveredNode;
function overNode(e) {
var node = e.data.node,
embedSettings = self.settings.embedObjects({
prefix: prefix
});
if (!embedSettings('enableHovering'))
return;
var hover = (renderers[node.type] || renderers.def).create(
node,
self.domElements.nodes[node.id],
self.measurementCanvas,
embedSettings
);
self.domElements.hovers[node.id] = hover;
// Inserting the hover in the dom
self.domElements.groups.hovers.appendChild(hover);
hoveredNode = node;
}
function outNode(e) {
var node = e.data.node,
embedSettings = self.settings.embedObjects({
prefix: prefix
});
if (!embedSettings('enableHovering'))
return;
// Deleting element
self.domElements.groups.hovers.removeChild(
self.domElements.hovers[node.id]
);
hoveredNode = null;
delete self.domElements.hovers[node.id];
// Reinstate
self.domElements.groups.nodes.appendChild(
self.domElements.nodes[node.id]
);
}
// OPTIMIZE: perform a real update rather than a deletion
function update() {
if (!hoveredNode)
return;
var embedSettings = self.settings.embedObjects({
prefix: prefix
});
// Deleting element before update
self.domElements.groups.hovers.removeChild(
self.domElements.hovers[hoveredNode.id]
);
delete self.domElements.hovers[hoveredNode.id];
var hover = (renderers[hoveredNode.type] || renderers.def).create(
hoveredNode,
self.domElements.nodes[hoveredNode.id],
self.measurementCanvas,
embedSettings
);
self.domElements.hovers[hoveredNode.id] = hover;
// Inserting the hover in the dom
self.domElements.groups.hovers.appendChild(hover);
}
// Binding events
this.bind('overNode', overNode);
this.bind('outNode', outNode);
// Update on render
this.bind('render', update);
};
/**
* This method resizes each DOM elements in the container and stores the new
* dimensions. Then, it renders the graph.
*
* @param {?number} width The new width of the container.
* @param {?number} height The new height of the container.
* @return {sigma.renderers.svg} Returns the instance itself.
*/
sigma.renderers.svg.prototype.resize = function(w, h) {
var oldWidth = this.width,
oldHeight = this.height,
pixelRatio = 1;
if (w !== undefined && h !== undefined) {
this.width = w;
this.height = h;
} else {
this.width = this.container.offsetWidth;
this.height = this.container.offsetHeight;
w = this.width;
h = this.height;
}
if (oldWidth !== this.width || oldHeight !== this.height) {
this.domElements.graph.style.width = w + 'px';
this.domElements.graph.style.height = h + 'px';
if (this.domElements.graph.tagName.toLowerCase() === 'svg') {
this.domElements.graph.setAttribute('width', (w * pixelRatio));
this.domElements.graph.setAttribute('height', (h * pixelRatio));
}
}
return this;
};
/**
* The labels, nodes and edges renderers are stored in the three following
* objects. When an element is drawn, its type will be checked and if a
* renderer with the same name exists, it will be used. If not found, the
* default renderer will be used instead.
*
* They are stored in different files, in the "./svg" folder.
*/
sigma.utils.pkg('sigma.svg.nodes');
sigma.utils.pkg('sigma.svg.edges');
sigma.utils.pkg('sigma.svg.labels');
}).call(this);

View File

@ -0,0 +1,693 @@
;(function(undefined) {
'use strict';
if (typeof sigma === 'undefined')
throw 'sigma is not declared';
// Initialize packages:
sigma.utils.pkg('sigma.renderers');
/**
* This function is the constructor of the canvas sigma's renderer.
*
* @param {sigma.classes.graph} graph The graph to render.
* @param {sigma.classes.camera} camera The camera.
* @param {configurable} settings The sigma instance settings
* function.
* @param {object} object The options object.
* @return {sigma.renderers.canvas} The renderer instance.
*/
sigma.renderers.webgl = function(graph, camera, settings, options) {
if (typeof options !== 'object')
throw 'sigma.renderers.webgl: Wrong arguments.';
if (!(options.container instanceof HTMLElement))
throw 'Container not found.';
var k,
i,
l,
a,
fn,
_self = this;
sigma.classes.dispatcher.extend(this);
// Conrad related attributes:
this.jobs = {};
Object.defineProperty(this, 'conradId', {
value: sigma.utils.id()
});
// Initialize main attributes:
this.graph = graph;
this.camera = camera;
this.contexts = {};
this.domElements = {};
this.options = options;
this.container = this.options.container;
this.settings = (
typeof options.settings === 'object' &&
options.settings
) ?
settings.embedObjects(options.settings) :
settings;
// Find the prefix:
this.options.prefix = this.camera.readPrefix;
// Initialize programs hash
Object.defineProperty(this, 'nodePrograms', {
value: {}
});
Object.defineProperty(this, 'edgePrograms', {
value: {}
});
Object.defineProperty(this, 'nodeFloatArrays', {
value: {}
});
Object.defineProperty(this, 'edgeFloatArrays', {
value: {}
});
// Initialize the DOM elements:
if (this.settings(options, 'batchEdgesDrawing')) {
this.initDOM('canvas', 'edges', true);
this.initDOM('canvas', 'nodes', true);
} else {
this.initDOM('canvas', 'scene', true);
this.contexts.nodes = this.contexts.scene;
this.contexts.edges = this.contexts.scene;
}
this.initDOM('canvas', 'labels');
this.initDOM('canvas', 'mouse');
this.contexts.hover = this.contexts.mouse;
// Initialize captors:
this.captors = [];
a = this.options.captors || [sigma.captors.mouse, sigma.captors.touch];
for (i = 0, l = a.length; i < l; i++) {
fn = typeof a[i] === 'function' ? a[i] : sigma.captors[a[i]];
this.captors.push(
new fn(
this.domElements.mouse,
this.camera,
this.settings
)
);
}
// Deal with sigma events:
sigma.misc.bindEvents.call(this, this.camera.prefix);
sigma.misc.drawHovers.call(this, this.camera.prefix);
this.resize();
};
/**
* This method will generate the nodes and edges float arrays. This step is
* separated from the "render" method, because to keep WebGL efficient, since
* all the camera and middlewares are modelised as matrices and they do not
* require the float arrays to be regenerated.
*
* Basically, when the user moves the camera or applies some specific linear
* transformations, this process step will be skipped, and the "render"
* method will efficiently refresh the rendering.
*
* And when the user modifies the graph colors or positions (applying a new
* layout or filtering the colors, for instance), this "process" step will be
* required to regenerate the float arrays.
*
* @return {sigma.renderers.webgl} Returns the instance itself.
*/
sigma.renderers.webgl.prototype.process = function() {
var a,
i,
l,
k,
type,
renderer,
graph = this.graph,
options = sigma.utils.extend(options, this.options);
// Empty float arrays:
for (k in this.nodeFloatArrays)
delete this.nodeFloatArrays[k];
for (k in this.edgeFloatArrays)
delete this.edgeFloatArrays[k];
// Sort edges and nodes per types:
for (a = graph.edges(), i = 0, l = a.length; i < l; i++) {
type = a[i].type || this.settings(options, 'defaultEdgeType');
k = (type && sigma.webgl.edges[type]) ? type : 'def';
if (!this.edgeFloatArrays[k])
this.edgeFloatArrays[k] = {
edges: []
};
this.edgeFloatArrays[k].edges.push(a[i]);
}
for (a = graph.nodes(), i = 0, l = a.length; i < l; i++) {
type = a[i].type || this.settings(options, 'defaultNodeType');
k = (type && sigma.webgl.nodes[type]) ? type : 'def';
if (!this.nodeFloatArrays[k])
this.nodeFloatArrays[k] = {
nodes: []
};
this.nodeFloatArrays[k].nodes.push(a[i]);
}
// Push edges:
for (k in this.edgeFloatArrays) {
renderer = sigma.webgl.edges[k];
for (a = this.edgeFloatArrays[k].edges, i = 0, l = a.length; i < l; i++) {
if (!this.edgeFloatArrays[k].array)
this.edgeFloatArrays[k].array = new Float32Array(
a.length * renderer.POINTS * renderer.ATTRIBUTES
);
// Just check that the edge and both its extremities are visible:
if (
!a[i].hidden &&
!graph.nodes(a[i].source).hidden &&
!graph.nodes(a[i].target).hidden
)
renderer.addEdge(
a[i],
graph.nodes(a[i].source),
graph.nodes(a[i].target),
this.edgeFloatArrays[k].array,
i * renderer.POINTS * renderer.ATTRIBUTES,
options.prefix,
this.settings
);
}
}
// Push nodes:
for (k in this.nodeFloatArrays) {
renderer = sigma.webgl.nodes[k];
for (a = this.nodeFloatArrays[k].nodes, i = 0, l = a.length; i < l; i++) {
if (!this.nodeFloatArrays[k].array)
this.nodeFloatArrays[k].array = new Float32Array(
a.length * renderer.POINTS * renderer.ATTRIBUTES
);
// Just check that the edge and both its extremities are visible:
if (
!a[i].hidden
)
renderer.addNode(
a[i],
this.nodeFloatArrays[k].array,
i * renderer.POINTS * renderer.ATTRIBUTES,
options.prefix,
this.settings
);
}
}
return this;
};
/**
* This method renders the graph. It basically calls each program (and
* generate them if they do not exist yet) to render nodes and edges, batched
* per renderer.
*
* As in the canvas renderer, it is possible to display edges, nodes and / or
* labels in batches, to make the whole thing way more scalable.
*
* @param {?object} params Eventually an object of options.
* @return {sigma.renderers.webgl} Returns the instance itself.
*/
sigma.renderers.webgl.prototype.render = function(params) {
var a,
i,
l,
k,
o,
program,
renderer,
self = this,
graph = this.graph,
nodesGl = this.contexts.nodes,
edgesGl = this.contexts.edges,
matrix = this.camera.getMatrix(),
options = sigma.utils.extend(params, this.options),
drawLabels = this.settings(options, 'drawLabels'),
drawEdges = this.settings(options, 'drawEdges'),
drawNodes = this.settings(options, 'drawNodes');
// Call the resize function:
this.resize(false);
// Check the 'hideEdgesOnMove' setting:
if (this.settings(options, 'hideEdgesOnMove'))
if (this.camera.isAnimated || this.camera.isMoving)
drawEdges = false;
// Clear canvases:
this.clear();
// Translate matrix to [width/2, height/2]:
matrix = sigma.utils.matrices.multiply(
matrix,
sigma.utils.matrices.translation(this.width / 2, this.height / 2)
);
// Kill running jobs:
for (k in this.jobs)
if (conrad.hasJob(k))
conrad.killJob(k);
if (drawEdges) {
if (this.settings(options, 'batchEdgesDrawing'))
(function() {
var a,
k,
i,
id,
job,
arr,
end,
start,
renderer,
batchSize,
currentProgram;
id = 'edges_' + this.conradId;
batchSize = this.settings(options, 'webglEdgesBatchSize');
a = Object.keys(this.edgeFloatArrays);
if (!a.length)
return;
i = 0;
renderer = sigma.webgl.edges[a[i]];
arr = this.edgeFloatArrays[a[i]].array;
start = 0;
end = Math.min(
start + batchSize * renderer.POINTS,
arr.length / renderer.ATTRIBUTES
);
job = function() {
// Check program:
if (!this.edgePrograms[a[i]])
this.edgePrograms[a[i]] = renderer.initProgram(edgesGl);
if (start < end) {
edgesGl.useProgram(this.edgePrograms[a[i]]);
renderer.render(
edgesGl,
this.edgePrograms[a[i]],
arr,
{
settings: this.settings,
matrix: matrix,
width: this.width,
height: this.height,
ratio: this.camera.ratio,
scalingRatio: this.settings(
options,
'webglOversamplingRatio'
),
start: start,
count: end - start
}
);
}
// Catch job's end:
if (
end >= arr.length / renderer.ATTRIBUTES &&
i === a.length - 1
) {
delete this.jobs[id];
return false;
}
if (end >= arr.length / renderer.ATTRIBUTES) {
i++;
arr = this.edgeFloatArrays[a[i]].array;
renderer = sigma.webgl.edges[a[i]];
start = 0;
end = Math.min(
start + batchSize * renderer.POINTS,
arr.length / renderer.ATTRIBUTES
);
} else {
start = end;
end = Math.min(
start + batchSize * renderer.POINTS,
arr.length / renderer.ATTRIBUTES
);
}
return true;
};
this.jobs[id] = job;
conrad.addJob(id, job.bind(this));
}).call(this);
else {
for (k in this.edgeFloatArrays) {
renderer = sigma.webgl.edges[k];
// Check program:
if (!this.edgePrograms[k])
this.edgePrograms[k] = renderer.initProgram(edgesGl);
// Render
if (this.edgeFloatArrays[k]) {
edgesGl.useProgram(this.edgePrograms[k]);
renderer.render(
edgesGl,
this.edgePrograms[k],
this.edgeFloatArrays[k].array,
{
settings: this.settings,
matrix: matrix,
width: this.width,
height: this.height,
ratio: this.camera.ratio,
scalingRatio: this.settings(options, 'webglOversamplingRatio')
}
);
}
}
}
}
if (drawNodes) {
// Enable blending:
nodesGl.blendFunc(nodesGl.SRC_ALPHA, nodesGl.ONE_MINUS_SRC_ALPHA);
nodesGl.enable(nodesGl.BLEND);
for (k in this.nodeFloatArrays) {
renderer = sigma.webgl.nodes[k];
// Check program:
if (!this.nodePrograms[k])
this.nodePrograms[k] = renderer.initProgram(nodesGl);
// Render
if (this.nodeFloatArrays[k]) {
nodesGl.useProgram(this.nodePrograms[k]);
renderer.render(
nodesGl,
this.nodePrograms[k],
this.nodeFloatArrays[k].array,
{
settings: this.settings,
matrix: matrix,
width: this.width,
height: this.height,
ratio: this.camera.ratio,
scalingRatio: this.settings(options, 'webglOversamplingRatio')
}
);
}
}
}
if (drawLabels) {
a = this.camera.quadtree.area(
this.camera.getRectangle(this.width, this.height)
);
// Apply camera view to these nodes:
this.camera.applyView(
undefined,
undefined,
{
nodes: a,
edges: [],
width: this.width,
height: this.height
}
);
o = function(key) {
return self.settings({
prefix: self.camera.prefix
}, key);
};
for (i = 0, l = a.length; i < l; i++)
if (!a[i].hidden)
(
sigma.canvas.labels[
a[i].type ||
this.settings(options, 'defaultNodeType')
] || sigma.canvas.labels.def
)(a[i], this.contexts.labels, o);
}
this.dispatchEvent('render');
return this;
};
/**
* This method creates a DOM element of the specified type, switches its
* position to "absolute", references it to the domElements attribute, and
* finally appends it to the container.
*
* @param {string} tag The label tag.
* @param {string} id The id of the element (to store it in
* "domElements").
* @param {?boolean} webgl Will init the WebGL context if true.
*/
sigma.renderers.webgl.prototype.initDOM = function(tag, id, webgl) {
var gl,
dom = document.createElement(tag),
self = this;
dom.style.position = 'absolute';
dom.setAttribute('class', 'sigma-' + id);
this.domElements[id] = dom;
this.container.appendChild(dom);
if (tag.toLowerCase() === 'canvas') {
this.contexts[id] = dom.getContext(webgl ? 'experimental-webgl' : '2d', {
preserveDrawingBuffer: true
});
// Adding webgl context loss listeners
if (webgl) {
dom.addEventListener('webglcontextlost', function(e) {
e.preventDefault();
}, false);
dom.addEventListener('webglcontextrestored', function(e) {
self.render();
}, false);
}
}
};
/**
* This method resizes each DOM elements in the container and stores the new
* dimensions. Then, it renders the graph.
*
* @param {?number} width The new width of the container.
* @param {?number} height The new height of the container.
* @return {sigma.renderers.webgl} Returns the instance itself.
*/
sigma.renderers.webgl.prototype.resize = function(w, h) {
var k,
oldWidth = this.width,
oldHeight = this.height;
if (w !== undefined && h !== undefined) {
this.width = w;
this.height = h;
} else {
this.width = this.container.offsetWidth;
this.height = this.container.offsetHeight;
w = this.width;
h = this.height;
}
if (oldWidth !== this.width || oldHeight !== this.height) {
for (k in this.domElements) {
this.domElements[k].style.width = w + 'px';
this.domElements[k].style.height = h + 'px';
if (this.domElements[k].tagName.toLowerCase() === 'canvas') {
// If simple 2D canvas:
if (this.contexts[k] && this.contexts[k].scale) {
this.domElements[k].setAttribute('width', w + 'px');
this.domElements[k].setAttribute('height', h + 'px');
} else {
this.domElements[k].setAttribute(
'width',
(w * this.settings('webglOversamplingRatio')) + 'px'
);
this.domElements[k].setAttribute(
'height',
(h * this.settings('webglOversamplingRatio')) + 'px'
);
}
}
}
}
// Scale:
for (k in this.contexts)
if (this.contexts[k] && this.contexts[k].viewport)
this.contexts[k].viewport(
0,
0,
this.width * this.settings('webglOversamplingRatio'),
this.height * this.settings('webglOversamplingRatio')
);
return this;
};
/**
* This method clears each canvas.
*
* @return {sigma.renderers.webgl} Returns the instance itself.
*/
sigma.renderers.webgl.prototype.clear = function() {
var k;
for (k in this.domElements)
if (this.domElements[k].tagName === 'CANVAS')
this.domElements[k].width = this.domElements[k].width;
this.contexts.nodes.clear(this.contexts.nodes.COLOR_BUFFER_BIT);
this.contexts.edges.clear(this.contexts.edges.COLOR_BUFFER_BIT);
return this;
};
/**
* This method kills contexts and other attributes.
*/
sigma.renderers.webgl.prototype.kill = function() {
var k,
captor;
// Kill captors:
while ((captor = this.captors.pop()))
captor.kill();
delete this.captors;
// Kill contexts:
for (k in this.domElements) {
this.domElements[k].parentNode.removeChild(this.domElements[k]);
delete this.domElements[k];
delete this.contexts[k];
}
delete this.domElements;
delete this.contexts;
};
/**
* The object "sigma.webgl.nodes" contains the different WebGL node
* renderers. The default one draw nodes as discs. Here are the attributes
* any node renderer must have:
*
* {number} POINTS The number of points required to draw a node.
* {number} ATTRIBUTES The number of attributes needed to draw one point.
* {function} addNode A function that adds a node to the data stack that
* will be given to the buffer. Here is the arguments:
* > {object} node
* > {number} index The node index in the
* nodes array.
* > {Float32Array} data The stack.
* > {object} options Some options.
* {function} render The function that will effectively render the nodes
* into the buffer.
* > {WebGLRenderingContext} gl
* > {WebGLProgram} program
* > {Float32Array} data The stack to give to the
* buffer.
* > {object} params An object containing some
* options, like width,
* height, the camera ratio.
* {function} initProgram The function that will initiate the program, with
* the relevant shaders and parameters. It must return
* the newly created program.
*
* Check sigma.webgl.nodes.def or sigma.webgl.nodes.fast to see how it
* works more precisely.
*/
sigma.utils.pkg('sigma.webgl.nodes');
/**
* The object "sigma.webgl.edges" contains the different WebGL edge
* renderers. The default one draw edges as direct lines. Here are the
* attributes any edge renderer must have:
*
* {number} POINTS The number of points required to draw an edge.
* {number} ATTRIBUTES The number of attributes needed to draw one point.
* {function} addEdge A function that adds an edge to the data stack that
* will be given to the buffer. Here is the arguments:
* > {object} edge
* > {object} source
* > {object} target
* > {Float32Array} data The stack.
* > {object} options Some options.
* {function} render The function that will effectively render the edges
* into the buffer.
* > {WebGLRenderingContext} gl
* > {WebGLProgram} program
* > {Float32Array} data The stack to give to the
* buffer.
* > {object} params An object containing some
* options, like width,
* height, the camera ratio.
* {function} initProgram The function that will initiate the program, with
* the relevant shaders and parameters. It must return
* the newly created program.
*
* Check sigma.webgl.edges.def or sigma.webgl.edges.fast to see how it
* works more precisely.
*/
sigma.utils.pkg('sigma.webgl.edges');
/**
* The object "sigma.canvas.labels" contains the different
* label renderers for the WebGL renderer. Since displaying texts in WebGL is
* definitely painful and since there a way less labels to display than nodes
* or edges, the default renderer simply renders them in a canvas.
*
* A labels renderer is a simple function, taking as arguments the related
* node, the renderer and a settings function.
*/
sigma.utils.pkg('sigma.canvas.labels');
}).call(this);

View File

@ -0,0 +1,84 @@
;(function() {
'use strict';
sigma.utils.pkg('sigma.svg.edges');
/**
* The curve edge renderer. It renders the node as a bezier curve.
*/
sigma.svg.edges.curve = {
/**
* SVG Element creation.
*
* @param {object} edge The edge object.
* @param {object} source The source node object.
* @param {object} target The target node object.
* @param {configurable} settings The settings function.
*/
create: function(edge, source, target, settings) {
var color = edge.color,
prefix = settings('prefix') || '',
edgeColor = settings('edgeColor'),
defaultNodeColor = settings('defaultNodeColor'),
defaultEdgeColor = settings('defaultEdgeColor');
if (!color)
switch (edgeColor) {
case 'source':
color = source.color || defaultNodeColor;
break;
case 'target':
color = target.color || defaultNodeColor;
break;
default:
color = defaultEdgeColor;
break;
}
var path = document.createElementNS(settings('xmlns'), 'path');
// Attributes
path.setAttributeNS(null, 'data-edge-id', edge.id);
path.setAttributeNS(null, 'class', settings('classPrefix') + '-edge');
path.setAttributeNS(null, 'stroke', color);
return path;
},
/**
* SVG Element update.
*
* @param {object} edge The edge object.
* @param {DOMElement} line The line DOM Element.
* @param {object} source The source node object.
* @param {object} target The target node object.
* @param {configurable} settings The settings function.
*/
update: function(edge, path, source, target, settings) {
var prefix = settings('prefix') || '';
path.setAttributeNS(null, 'stroke-width', edge[prefix + 'size'] || 1);
// Control point
var cx = (source[prefix + 'x'] + target[prefix + 'x']) / 2 +
(target[prefix + 'y'] - source[prefix + 'y']) / 4,
cy = (source[prefix + 'y'] + target[prefix + 'y']) / 2 +
(source[prefix + 'x'] - target[prefix + 'x']) / 4;
// Path
var p = 'M' + source[prefix + 'x'] + ',' + source[prefix + 'y'] + ' ' +
'Q' + cx + ',' + cy + ' ' +
target[prefix + 'x'] + ',' + target[prefix + 'y'];
// Updating attributes
path.setAttributeNS(null, 'd', p);
path.setAttributeNS(null, 'fill', 'none');
// Showing
path.style.display = '';
return this;
}
};
})();

View File

@ -0,0 +1,73 @@
;(function() {
'use strict';
sigma.utils.pkg('sigma.svg.edges');
/**
* The default edge renderer. It renders the node as a simple line.
*/
sigma.svg.edges.def = {
/**
* SVG Element creation.
*
* @param {object} edge The edge object.
* @param {object} source The source node object.
* @param {object} target The target node object.
* @param {configurable} settings The settings function.
*/
create: function(edge, source, target, settings) {
var color = edge.color,
prefix = settings('prefix') || '',
edgeColor = settings('edgeColor'),
defaultNodeColor = settings('defaultNodeColor'),
defaultEdgeColor = settings('defaultEdgeColor');
if (!color)
switch (edgeColor) {
case 'source':
color = source.color || defaultNodeColor;
break;
case 'target':
color = target.color || defaultNodeColor;
break;
default:
color = defaultEdgeColor;
break;
}
var line = document.createElementNS(settings('xmlns'), 'line');
// Attributes
line.setAttributeNS(null, 'data-edge-id', edge.id);
line.setAttributeNS(null, 'class', settings('classPrefix') + '-edge');
line.setAttributeNS(null, 'stroke', color);
return line;
},
/**
* SVG Element update.
*
* @param {object} edge The edge object.
* @param {DOMElement} line The line DOM Element.
* @param {object} source The source node object.
* @param {object} target The target node object.
* @param {configurable} settings The settings function.
*/
update: function(edge, line, source, target, settings) {
var prefix = settings('prefix') || '';
line.setAttributeNS(null, 'stroke-width', edge[prefix + 'size'] || 1);
line.setAttributeNS(null, 'x1', source[prefix + 'x']);
line.setAttributeNS(null, 'y1', source[prefix + 'y']);
line.setAttributeNS(null, 'x2', target[prefix + 'x']);
line.setAttributeNS(null, 'y2', target[prefix + 'y']);
// Showing
line.style.display = '';
return this;
}
};
})();

View File

@ -0,0 +1,113 @@
;(function(undefined) {
'use strict';
if (typeof sigma === 'undefined')
throw 'sigma is not declared';
// Initialize packages:
sigma.utils.pkg('sigma.svg.hovers');
/**
* The default hover renderer.
*/
sigma.svg.hovers.def = {
/**
* SVG Element creation.
*
* @param {object} node The node object.
* @param {CanvasElement} measurementCanvas A fake canvas handled by
* the svg to perform some measurements and
* passed by the renderer.
* @param {DOMElement} nodeCircle The node DOM Element.
* @param {configurable} settings The settings function.
*/
create: function(node, nodeCircle, measurementCanvas, settings) {
// Defining visual properties
var x,
y,
w,
h,
e,
d,
fontStyle = settings('hoverFontStyle') || settings('fontStyle'),
prefix = settings('prefix') || '',
size = node[prefix + 'size'],
fontSize = (settings('labelSize') === 'fixed') ?
settings('defaultLabelSize') :
settings('labelSizeRatio') * size,
fontColor = (settings('labelHoverColor') === 'node') ?
(node.color || settings('defaultNodeColor')) :
settings('defaultLabelHoverColor');
// Creating elements
var group = document.createElementNS(settings('xmlns'), 'g'),
rectangle = document.createElementNS(settings('xmlns'), 'rect'),
circle = document.createElementNS(settings('xmlns'), 'circle'),
text = document.createElementNS(settings('xmlns'), 'text');
// Defining properties
group.setAttributeNS(null, 'class', settings('classPrefix') + '-hover');
group.setAttributeNS(null, 'data-node-id', node.id);
if (typeof node.label === 'string') {
// Text
text.innerHTML = node.label;
text.textContent = node.label;
text.setAttributeNS(
null,
'class',
settings('classPrefix') + '-hover-label');
text.setAttributeNS(null, 'font-size', fontSize);
text.setAttributeNS(null, 'font-family', settings('font'));
text.setAttributeNS(null, 'fill', fontColor);
text.setAttributeNS(null, 'x',
Math.round(node[prefix + 'x'] + size + 3));
text.setAttributeNS(null, 'y',
Math.round(node[prefix + 'y'] + fontSize / 3));
// Measures
// OPTIMIZE: Find a better way than a measurement canvas
x = Math.round(node[prefix + 'x'] - fontSize / 2 - 2);
y = Math.round(node[prefix + 'y'] - fontSize / 2 - 2);
w = Math.round(
measurementCanvas.measureText(node.label).width +
fontSize / 2 + size + 9
);
h = Math.round(fontSize + 4);
e = Math.round(fontSize / 2 + 2);
// Circle
circle.setAttributeNS(
null,
'class',
settings('classPrefix') + '-hover-area');
circle.setAttributeNS(null, 'fill', '#fff');
circle.setAttributeNS(null, 'cx', node[prefix + 'x']);
circle.setAttributeNS(null, 'cy', node[prefix + 'y']);
circle.setAttributeNS(null, 'r', e);
// Rectangle
rectangle.setAttributeNS(
null,
'class',
settings('classPrefix') + '-hover-area');
rectangle.setAttributeNS(null, 'fill', '#fff');
rectangle.setAttributeNS(null, 'x', node[prefix + 'x'] + e / 4);
rectangle.setAttributeNS(null, 'y', node[prefix + 'y'] - e);
rectangle.setAttributeNS(null, 'width', w);
rectangle.setAttributeNS(null, 'height', h);
}
// Appending childs
group.appendChild(circle);
group.appendChild(rectangle);
group.appendChild(text);
group.appendChild(nodeCircle);
return group;
}
};
}).call(this);

View File

@ -0,0 +1,80 @@
;(function(undefined) {
'use strict';
if (typeof sigma === 'undefined')
throw 'sigma is not declared';
// Initialize packages:
sigma.utils.pkg('sigma.svg.labels');
/**
* The default label renderer. It renders the label as a simple text.
*/
sigma.svg.labels.def = {
/**
* SVG Element creation.
*
* @param {object} node The node object.
* @param {configurable} settings The settings function.
*/
create: function(node, settings) {
var prefix = settings('prefix') || '',
size = node[prefix + 'size'],
text = document.createElementNS(settings('xmlns'), 'text');
var fontSize = (settings('labelSize') === 'fixed') ?
settings('defaultLabelSize') :
settings('labelSizeRatio') * size;
var fontColor = (settings('labelColor') === 'node') ?
(node.color || settings('defaultNodeColor')) :
settings('defaultLabelColor');
text.setAttributeNS(null, 'data-label-target', node.id);
text.setAttributeNS(null, 'class', settings('classPrefix') + '-label');
text.setAttributeNS(null, 'font-size', fontSize);
text.setAttributeNS(null, 'font-family', settings('font'));
text.setAttributeNS(null, 'fill', fontColor);
text.innerHTML = node.label;
text.textContent = node.label;
return text;
},
/**
* SVG Element update.
*
* @param {object} node The node object.
* @param {DOMElement} text The label DOM element.
* @param {configurable} settings The settings function.
*/
update: function(node, text, settings) {
var prefix = settings('prefix') || '',
size = node[prefix + 'size'];
var fontSize = (settings('labelSize') === 'fixed') ?
settings('defaultLabelSize') :
settings('labelSizeRatio') * size;
// Case when we don't want to display the label
if (!settings('forceLabels') && size < settings('labelThreshold'))
return;
if (typeof node.label !== 'string')
return;
// Updating
text.setAttributeNS(null, 'x',
Math.round(node[prefix + 'x'] + size + 3));
text.setAttributeNS(null, 'y',
Math.round(node[prefix + 'y'] + fontSize / 3));
// Showing
text.style.display = '';
return this;
}
};
}).call(this);

View File

@ -0,0 +1,58 @@
;(function() {
'use strict';
sigma.utils.pkg('sigma.svg.nodes');
/**
* The default node renderer. It renders the node as a simple disc.
*/
sigma.svg.nodes.def = {
/**
* SVG Element creation.
*
* @param {object} node The node object.
* @param {configurable} settings The settings function.
*/
create: function(node, settings) {
var prefix = settings('prefix') || '',
circle = document.createElementNS(settings('xmlns'), 'circle');
// Defining the node's circle
circle.setAttributeNS(null, 'data-node-id', node.id);
circle.setAttributeNS(null, 'class', settings('classPrefix') + '-node');
circle.setAttributeNS(
null, 'fill', node.color || settings('defaultNodeColor'));
// Returning the DOM Element
return circle;
},
/**
* SVG Element update.
*
* @param {object} node The node object.
* @param {DOMElement} circle The node DOM element.
* @param {configurable} settings The settings function.
*/
update: function(node, circle, settings) {
var prefix = settings('prefix') || '';
// Applying changes
// TODO: optimize - check if necessary
circle.setAttributeNS(null, 'cx', node[prefix + 'x']);
circle.setAttributeNS(null, 'cy', node[prefix + 'y']);
circle.setAttributeNS(null, 'r', node[prefix + 'size']);
// Updating only if not freestyle
if (!settings('freeStyle'))
circle.setAttributeNS(
null, 'fill', node.color || settings('defaultNodeColor'));
// Showing
circle.style.display = '';
return this;
}
};
})();

View File

@ -0,0 +1,31 @@
;(function() {
'use strict';
sigma.utils.pkg('sigma.svg.utils');
/**
* Some useful functions used by sigma's SVG renderer.
*/
sigma.svg.utils = {
/**
* SVG Element show.
*
* @param {DOMElement} element The DOM element to show.
*/
show: function(element) {
element.style.display = '';
return this;
},
/**
* SVG Element hide.
*
* @param {DOMElement} element The DOM element to hide.
*/
hide: function(element) {
element.style.display = 'none';
return this;
}
};
})();

View File

@ -0,0 +1,391 @@
;(function() {
'use strict';
sigma.utils.pkg('sigma.webgl.edges');
/**
* This edge renderer will display edges as arrows going from the source node
* to the target node. To deal with edge thicknesses, the lines are made of
* three triangles: two forming rectangles, with the gl.TRIANGLES drawing
* mode.
*
* It is expensive, since drawing a single edge requires 9 points, each
* having a lot of attributes.
*/
sigma.webgl.edges.arrow = {
POINTS: 9,
ATTRIBUTES: 11,
addEdge: function(edge, source, target, data, i, prefix, settings) {
var w = (edge[prefix + 'size'] || 1) / 2,
x1 = source[prefix + 'x'],
y1 = source[prefix + 'y'],
x2 = target[prefix + 'x'],
y2 = target[prefix + 'y'],
targetSize = target[prefix + 'size'],
color = edge.color;
if (!color)
switch (settings('edgeColor')) {
case 'source':
color = source.color || settings('defaultNodeColor');
break;
case 'target':
color = target.color || settings('defaultNodeColor');
break;
default:
color = settings('defaultEdgeColor');
break;
}
// Normalize color:
color = sigma.utils.floatColor(color);
data[i++] = x1;
data[i++] = y1;
data[i++] = x2;
data[i++] = y2;
data[i++] = w;
data[i++] = targetSize;
data[i++] = 0.0;
data[i++] = 0.0;
data[i++] = 0.0;
data[i++] = 0.0;
data[i++] = color;
data[i++] = x2;
data[i++] = y2;
data[i++] = x1;
data[i++] = y1;
data[i++] = w;
data[i++] = targetSize;
data[i++] = 1.0;
data[i++] = 1.0;
data[i++] = 0.0;
data[i++] = 0.0;
data[i++] = color;
data[i++] = x2;
data[i++] = y2;
data[i++] = x1;
data[i++] = y1;
data[i++] = w;
data[i++] = targetSize;
data[i++] = 1.0;
data[i++] = 0.0;
data[i++] = 0.0;
data[i++] = 0.0;
data[i++] = color;
data[i++] = x2;
data[i++] = y2;
data[i++] = x1;
data[i++] = y1;
data[i++] = w;
data[i++] = targetSize;
data[i++] = 1.0;
data[i++] = 0.0;
data[i++] = 0.0;
data[i++] = 0.0;
data[i++] = color;
data[i++] = x1;
data[i++] = y1;
data[i++] = x2;
data[i++] = y2;
data[i++] = w;
data[i++] = targetSize;
data[i++] = 0.0;
data[i++] = 1.0;
data[i++] = 0.0;
data[i++] = 0.0;
data[i++] = color;
data[i++] = x1;
data[i++] = y1;
data[i++] = x2;
data[i++] = y2;
data[i++] = w;
data[i++] = targetSize;
data[i++] = 0.0;
data[i++] = 0.0;
data[i++] = 0.0;
data[i++] = 0.0;
data[i++] = color;
// Arrow head:
data[i++] = x2;
data[i++] = y2;
data[i++] = x1;
data[i++] = y1;
data[i++] = w;
data[i++] = targetSize;
data[i++] = 1.0;
data[i++] = 0.0;
data[i++] = 1.0;
data[i++] = -1.0;
data[i++] = color;
data[i++] = x2;
data[i++] = y2;
data[i++] = x1;
data[i++] = y1;
data[i++] = w;
data[i++] = targetSize;
data[i++] = 1.0;
data[i++] = 0.0;
data[i++] = 1.0;
data[i++] = 0.0;
data[i++] = color;
data[i++] = x2;
data[i++] = y2;
data[i++] = x1;
data[i++] = y1;
data[i++] = w;
data[i++] = targetSize;
data[i++] = 1.0;
data[i++] = 0.0;
data[i++] = 1.0;
data[i++] = 1.0;
data[i++] = color;
},
render: function(gl, program, data, params) {
var buffer;
// Define attributes:
var positionLocation1 =
gl.getAttribLocation(program, 'a_pos1'),
positionLocation2 =
gl.getAttribLocation(program, 'a_pos2'),
thicknessLocation =
gl.getAttribLocation(program, 'a_thickness'),
targetSizeLocation =
gl.getAttribLocation(program, 'a_tSize'),
delayLocation =
gl.getAttribLocation(program, 'a_delay'),
minusLocation =
gl.getAttribLocation(program, 'a_minus'),
headLocation =
gl.getAttribLocation(program, 'a_head'),
headPositionLocation =
gl.getAttribLocation(program, 'a_headPosition'),
colorLocation =
gl.getAttribLocation(program, 'a_color'),
resolutionLocation =
gl.getUniformLocation(program, 'u_resolution'),
matrixLocation =
gl.getUniformLocation(program, 'u_matrix'),
matrixHalfPiLocation =
gl.getUniformLocation(program, 'u_matrixHalfPi'),
matrixHalfPiMinusLocation =
gl.getUniformLocation(program, 'u_matrixHalfPiMinus'),
ratioLocation =
gl.getUniformLocation(program, 'u_ratio'),
nodeRatioLocation =
gl.getUniformLocation(program, 'u_nodeRatio'),
arrowHeadLocation =
gl.getUniformLocation(program, 'u_arrowHead'),
scaleLocation =
gl.getUniformLocation(program, 'u_scale');
buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
gl.uniform2f(resolutionLocation, params.width, params.height);
gl.uniform1f(
ratioLocation,
params.ratio / Math.pow(params.ratio, params.settings('edgesPowRatio'))
);
gl.uniform1f(
nodeRatioLocation,
Math.pow(params.ratio, params.settings('nodesPowRatio')) /
params.ratio
);
gl.uniform1f(arrowHeadLocation, 5.0);
gl.uniform1f(scaleLocation, params.scalingRatio);
gl.uniformMatrix3fv(matrixLocation, false, params.matrix);
gl.uniformMatrix2fv(
matrixHalfPiLocation,
false,
sigma.utils.matrices.rotation(Math.PI / 2, true)
);
gl.uniformMatrix2fv(
matrixHalfPiMinusLocation,
false,
sigma.utils.matrices.rotation(-Math.PI / 2, true)
);
gl.enableVertexAttribArray(positionLocation1);
gl.enableVertexAttribArray(positionLocation2);
gl.enableVertexAttribArray(thicknessLocation);
gl.enableVertexAttribArray(targetSizeLocation);
gl.enableVertexAttribArray(delayLocation);
gl.enableVertexAttribArray(minusLocation);
gl.enableVertexAttribArray(headLocation);
gl.enableVertexAttribArray(headPositionLocation);
gl.enableVertexAttribArray(colorLocation);
gl.vertexAttribPointer(positionLocation1,
2,
gl.FLOAT,
false,
this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
0
);
gl.vertexAttribPointer(positionLocation2,
2,
gl.FLOAT,
false,
this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
8
);
gl.vertexAttribPointer(thicknessLocation,
1,
gl.FLOAT,
false,
this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
16
);
gl.vertexAttribPointer(targetSizeLocation,
1,
gl.FLOAT,
false,
this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
20
);
gl.vertexAttribPointer(delayLocation,
1,
gl.FLOAT,
false,
this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
24
);
gl.vertexAttribPointer(minusLocation,
1,
gl.FLOAT,
false,
this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
28
);
gl.vertexAttribPointer(headLocation,
1,
gl.FLOAT,
false,
this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
32
);
gl.vertexAttribPointer(headPositionLocation,
1,
gl.FLOAT,
false,
this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
36
);
gl.vertexAttribPointer(colorLocation,
1,
gl.FLOAT,
false,
this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
40
);
gl.drawArrays(
gl.TRIANGLES,
params.start || 0,
params.count || (data.length / this.ATTRIBUTES)
);
},
initProgram: function(gl) {
var vertexShader,
fragmentShader,
program;
vertexShader = sigma.utils.loadShader(
gl,
[
'attribute vec2 a_pos1;',
'attribute vec2 a_pos2;',
'attribute float a_thickness;',
'attribute float a_tSize;',
'attribute float a_delay;',
'attribute float a_minus;',
'attribute float a_head;',
'attribute float a_headPosition;',
'attribute float a_color;',
'uniform vec2 u_resolution;',
'uniform float u_ratio;',
'uniform float u_nodeRatio;',
'uniform float u_arrowHead;',
'uniform float u_scale;',
'uniform mat3 u_matrix;',
'uniform mat2 u_matrixHalfPi;',
'uniform mat2 u_matrixHalfPiMinus;',
'varying vec4 color;',
'void main() {',
// Find the good point:
'vec2 pos = normalize(a_pos2 - a_pos1);',
'mat2 matrix = (1.0 - a_head) *',
'(',
'a_minus * u_matrixHalfPiMinus +',
'(1.0 - a_minus) * u_matrixHalfPi',
') + a_head * (',
'a_headPosition * u_matrixHalfPiMinus * 0.6 +',
'(a_headPosition * a_headPosition - 1.0) * mat2(1.0)',
');',
'pos = a_pos1 + (',
// Deal with body:
'(1.0 - a_head) * a_thickness * u_ratio * matrix * pos +',
// Deal with head:
'a_head * u_arrowHead * a_thickness * u_ratio * matrix * pos +',
// Deal with delay:
'a_delay * pos * (',
'a_tSize / u_nodeRatio +',
'u_arrowHead * a_thickness * u_ratio',
')',
');',
// Scale from [[-1 1] [-1 1]] to the container:
'gl_Position = vec4(',
'((u_matrix * vec3(pos, 1)).xy /',
'u_resolution * 2.0 - 1.0) * vec2(1, -1),',
'0,',
'1',
');',
// Extract the color:
'float c = a_color;',
'color.b = mod(c, 256.0); c = floor(c / 256.0);',
'color.g = mod(c, 256.0); c = floor(c / 256.0);',
'color.r = mod(c, 256.0); c = floor(c / 256.0); color /= 255.0;',
'color.a = 1.0;',
'}'
].join('\n'),
gl.VERTEX_SHADER
);
fragmentShader = sigma.utils.loadShader(
gl,
[
'precision mediump float;',
'varying vec4 color;',
'void main(void) {',
'gl_FragColor = color;',
'}'
].join('\n'),
gl.FRAGMENT_SHADER
);
program = sigma.utils.loadProgram(gl, [vertexShader, fragmentShader]);
return program;
}
};
})();

View File

@ -0,0 +1,258 @@
;(function() {
'use strict';
sigma.utils.pkg('sigma.webgl.edges');
/**
* This edge renderer will display edges as lines going from the source node
* to the target node. To deal with edge thicknesses, the lines are made of
* two triangles forming rectangles, with the gl.TRIANGLES drawing mode.
*
* It is expensive, since drawing a single edge requires 6 points, each
* having 7 attributes (source position, target position, thickness, color
* and a flag indicating which vertice of the rectangle it is).
*/
sigma.webgl.edges.def = {
POINTS: 6,
ATTRIBUTES: 7,
addEdge: function(edge, source, target, data, i, prefix, settings) {
var w = (edge[prefix + 'size'] || 1) / 2,
x1 = source[prefix + 'x'],
y1 = source[prefix + 'y'],
x2 = target[prefix + 'x'],
y2 = target[prefix + 'y'],
color = edge.color;
if (!color)
switch (settings('edgeColor')) {
case 'source':
color = source.color || settings('defaultNodeColor');
break;
case 'target':
color = target.color || settings('defaultNodeColor');
break;
default:
color = settings('defaultEdgeColor');
break;
}
// Normalize color:
color = sigma.utils.floatColor(color);
data[i++] = x1;
data[i++] = y1;
data[i++] = x2;
data[i++] = y2;
data[i++] = w;
data[i++] = 0.0;
data[i++] = color;
data[i++] = x2;
data[i++] = y2;
data[i++] = x1;
data[i++] = y1;
data[i++] = w;
data[i++] = 1.0;
data[i++] = color;
data[i++] = x2;
data[i++] = y2;
data[i++] = x1;
data[i++] = y1;
data[i++] = w;
data[i++] = 0.0;
data[i++] = color;
data[i++] = x2;
data[i++] = y2;
data[i++] = x1;
data[i++] = y1;
data[i++] = w;
data[i++] = 0.0;
data[i++] = color;
data[i++] = x1;
data[i++] = y1;
data[i++] = x2;
data[i++] = y2;
data[i++] = w;
data[i++] = 1.0;
data[i++] = color;
data[i++] = x1;
data[i++] = y1;
data[i++] = x2;
data[i++] = y2;
data[i++] = w;
data[i++] = 0.0;
data[i++] = color;
},
render: function(gl, program, data, params) {
var buffer;
// Define attributes:
var colorLocation =
gl.getAttribLocation(program, 'a_color'),
positionLocation1 =
gl.getAttribLocation(program, 'a_position1'),
positionLocation2 =
gl.getAttribLocation(program, 'a_position2'),
thicknessLocation =
gl.getAttribLocation(program, 'a_thickness'),
minusLocation =
gl.getAttribLocation(program, 'a_minus'),
resolutionLocation =
gl.getUniformLocation(program, 'u_resolution'),
matrixLocation =
gl.getUniformLocation(program, 'u_matrix'),
matrixHalfPiLocation =
gl.getUniformLocation(program, 'u_matrixHalfPi'),
matrixHalfPiMinusLocation =
gl.getUniformLocation(program, 'u_matrixHalfPiMinus'),
ratioLocation =
gl.getUniformLocation(program, 'u_ratio'),
scaleLocation =
gl.getUniformLocation(program, 'u_scale');
buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
gl.uniform2f(resolutionLocation, params.width, params.height);
gl.uniform1f(
ratioLocation,
params.ratio / Math.pow(params.ratio, params.settings('edgesPowRatio'))
);
gl.uniform1f(scaleLocation, params.scalingRatio);
gl.uniformMatrix3fv(matrixLocation, false, params.matrix);
gl.uniformMatrix2fv(
matrixHalfPiLocation,
false,
sigma.utils.matrices.rotation(Math.PI / 2, true)
);
gl.uniformMatrix2fv(
matrixHalfPiMinusLocation,
false,
sigma.utils.matrices.rotation(-Math.PI / 2, true)
);
gl.enableVertexAttribArray(colorLocation);
gl.enableVertexAttribArray(positionLocation1);
gl.enableVertexAttribArray(positionLocation2);
gl.enableVertexAttribArray(thicknessLocation);
gl.enableVertexAttribArray(minusLocation);
gl.vertexAttribPointer(positionLocation1,
2,
gl.FLOAT,
false,
this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
0
);
gl.vertexAttribPointer(positionLocation2,
2,
gl.FLOAT,
false,
this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
8
);
gl.vertexAttribPointer(thicknessLocation,
1,
gl.FLOAT,
false,
this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
16
);
gl.vertexAttribPointer(minusLocation,
1,
gl.FLOAT,
false,
this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
20
);
gl.vertexAttribPointer(colorLocation,
1,
gl.FLOAT,
false,
this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
24
);
gl.drawArrays(
gl.TRIANGLES,
params.start || 0,
params.count || (data.length / this.ATTRIBUTES)
);
},
initProgram: function(gl) {
var vertexShader,
fragmentShader,
program;
vertexShader = sigma.utils.loadShader(
gl,
[
'attribute vec2 a_position1;',
'attribute vec2 a_position2;',
'attribute float a_thickness;',
'attribute float a_minus;',
'attribute float a_color;',
'uniform vec2 u_resolution;',
'uniform float u_ratio;',
'uniform float u_scale;',
'uniform mat3 u_matrix;',
'uniform mat2 u_matrixHalfPi;',
'uniform mat2 u_matrixHalfPiMinus;',
'varying vec4 color;',
'void main() {',
// Find the good point:
'vec2 position = a_thickness * u_ratio *',
'normalize(a_position2 - a_position1);',
'mat2 matrix = a_minus * u_matrixHalfPiMinus +',
'(1.0 - a_minus) * u_matrixHalfPi;',
'position = matrix * position + a_position1;',
// Scale from [[-1 1] [-1 1]] to the container:
'gl_Position = vec4(',
'((u_matrix * vec3(position, 1)).xy /',
'u_resolution * 2.0 - 1.0) * vec2(1, -1),',
'0,',
'1',
');',
// Extract the color:
'float c = a_color;',
'color.b = mod(c, 256.0); c = floor(c / 256.0);',
'color.g = mod(c, 256.0); c = floor(c / 256.0);',
'color.r = mod(c, 256.0); c = floor(c / 256.0); color /= 255.0;',
'color.a = 1.0;',
'}'
].join('\n'),
gl.VERTEX_SHADER
);
fragmentShader = sigma.utils.loadShader(
gl,
[
'precision mediump float;',
'varying vec4 color;',
'void main(void) {',
'gl_FragColor = color;',
'}'
].join('\n'),
gl.FRAGMENT_SHADER
);
program = sigma.utils.loadProgram(gl, [vertexShader, fragmentShader]);
return program;
}
};
})();

View File

@ -0,0 +1,147 @@
;(function() {
'use strict';
sigma.utils.pkg('sigma.webgl.edges');
/**
* This edge renderer will display edges as lines with the gl.LINES display
* mode. Since this mode does not support well thickness, edges are all drawn
* with the same thickness (3px), independantly of the edge attributes or the
* zooming ratio.
*/
sigma.webgl.edges.fast = {
POINTS: 2,
ATTRIBUTES: 3,
addEdge: function(edge, source, target, data, i, prefix, settings) {
var w = (edge[prefix + 'size'] || 1) / 2,
x1 = source[prefix + 'x'],
y1 = source[prefix + 'y'],
x2 = target[prefix + 'x'],
y2 = target[prefix + 'y'],
color = edge.color;
if (!color)
switch (settings('edgeColor')) {
case 'source':
color = source.color || settings('defaultNodeColor');
break;
case 'target':
color = target.color || settings('defaultNodeColor');
break;
default:
color = settings('defaultEdgeColor');
break;
}
// Normalize color:
color = sigma.utils.floatColor(color);
data[i++] = x1;
data[i++] = y1;
data[i++] = color;
data[i++] = x2;
data[i++] = y2;
data[i++] = color;
},
render: function(gl, program, data, params) {
var buffer;
// Define attributes:
var colorLocation =
gl.getAttribLocation(program, 'a_color'),
positionLocation =
gl.getAttribLocation(program, 'a_position'),
resolutionLocation =
gl.getUniformLocation(program, 'u_resolution'),
matrixLocation =
gl.getUniformLocation(program, 'u_matrix');
buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW);
gl.uniform2f(resolutionLocation, params.width, params.height);
gl.uniformMatrix3fv(matrixLocation, false, params.matrix);
gl.enableVertexAttribArray(positionLocation);
gl.enableVertexAttribArray(colorLocation);
gl.vertexAttribPointer(positionLocation,
2,
gl.FLOAT,
false,
this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
0
);
gl.vertexAttribPointer(colorLocation,
1,
gl.FLOAT,
false,
this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
8
);
gl.lineWidth(3);
gl.drawArrays(
gl.LINES,
params.start || 0,
params.count || (data.length / this.ATTRIBUTES)
);
},
initProgram: function(gl) {
var vertexShader,
fragmentShader,
program;
vertexShader = sigma.utils.loadShader(
gl,
[
'attribute vec2 a_position;',
'attribute float a_color;',
'uniform vec2 u_resolution;',
'uniform mat3 u_matrix;',
'varying vec4 color;',
'void main() {',
// Scale from [[-1 1] [-1 1]] to the container:
'gl_Position = vec4(',
'((u_matrix * vec3(a_position, 1)).xy /',
'u_resolution * 2.0 - 1.0) * vec2(1, -1),',
'0,',
'1',
');',
// Extract the color:
'float c = a_color;',
'color.b = mod(c, 256.0); c = floor(c / 256.0);',
'color.g = mod(c, 256.0); c = floor(c / 256.0);',
'color.r = mod(c, 256.0); c = floor(c / 256.0); color /= 255.0;',
'color.a = 1.0;',
'}'
].join('\n'),
gl.VERTEX_SHADER
);
fragmentShader = sigma.utils.loadShader(
gl,
[
'precision mediump float;',
'varying vec4 color;',
'void main(void) {',
'gl_FragColor = color;',
'}'
].join('\n'),
gl.FRAGMENT_SHADER
);
program = sigma.utils.loadProgram(gl, [vertexShader, fragmentShader]);
return program;
}
};
})();

View File

@ -0,0 +1,201 @@
;(function() {
'use strict';
sigma.utils.pkg('sigma.webgl.nodes');
/**
* This node renderer will display nodes as discs, shaped in triangles with
* the gl.TRIANGLES display mode. So, to be more precise, to draw one node,
* it will store three times the center of node, with the color and the size,
* and an angle indicating which "corner" of the triangle to draw.
*
* The fragment shader does not deal with anti-aliasing, so make sure that
* you deal with it somewhere else in the code (by default, the WebGL
* renderer will oversample the rendering through the webglOversamplingRatio
* value).
*/
sigma.webgl.nodes.def = {
POINTS: 3,
ATTRIBUTES: 5,
addNode: function(node, data, i, prefix, settings) {
var color = sigma.utils.floatColor(
node.color || settings('defaultNodeColor')
);
data[i++] = node[prefix + 'x'];
data[i++] = node[prefix + 'y'];
data[i++] = node[prefix + 'size'];
data[i++] = color;
data[i++] = 0;
data[i++] = node[prefix + 'x'];
data[i++] = node[prefix + 'y'];
data[i++] = node[prefix + 'size'];
data[i++] = color;
data[i++] = 2 * Math.PI / 3;
data[i++] = node[prefix + 'x'];
data[i++] = node[prefix + 'y'];
data[i++] = node[prefix + 'size'];
data[i++] = color;
data[i++] = 4 * Math.PI / 3;
},
render: function(gl, program, data, params) {
var buffer;
// Define attributes:
var positionLocation =
gl.getAttribLocation(program, 'a_position'),
sizeLocation =
gl.getAttribLocation(program, 'a_size'),
colorLocation =
gl.getAttribLocation(program, 'a_color'),
angleLocation =
gl.getAttribLocation(program, 'a_angle'),
resolutionLocation =
gl.getUniformLocation(program, 'u_resolution'),
matrixLocation =
gl.getUniformLocation(program, 'u_matrix'),
ratioLocation =
gl.getUniformLocation(program, 'u_ratio'),
scaleLocation =
gl.getUniformLocation(program, 'u_scale');
buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW);
gl.uniform2f(resolutionLocation, params.width, params.height);
gl.uniform1f(
ratioLocation,
1 / Math.pow(params.ratio, params.settings('nodesPowRatio'))
);
gl.uniform1f(scaleLocation, params.scalingRatio);
gl.uniformMatrix3fv(matrixLocation, false, params.matrix);
gl.enableVertexAttribArray(positionLocation);
gl.enableVertexAttribArray(sizeLocation);
gl.enableVertexAttribArray(colorLocation);
gl.enableVertexAttribArray(angleLocation);
gl.vertexAttribPointer(
positionLocation,
2,
gl.FLOAT,
false,
this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
0
);
gl.vertexAttribPointer(
sizeLocation,
1,
gl.FLOAT,
false,
this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
8
);
gl.vertexAttribPointer(
colorLocation,
1,
gl.FLOAT,
false,
this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
12
);
gl.vertexAttribPointer(
angleLocation,
1,
gl.FLOAT,
false,
this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
16
);
gl.drawArrays(
gl.TRIANGLES,
params.start || 0,
params.count || (data.length / this.ATTRIBUTES)
);
},
initProgram: function(gl) {
var vertexShader,
fragmentShader,
program;
vertexShader = sigma.utils.loadShader(
gl,
[
'attribute vec2 a_position;',
'attribute float a_size;',
'attribute float a_color;',
'attribute float a_angle;',
'uniform vec2 u_resolution;',
'uniform float u_ratio;',
'uniform float u_scale;',
'uniform mat3 u_matrix;',
'varying vec4 color;',
'varying vec2 center;',
'varying float radius;',
'void main() {',
// Multiply the point size twice:
'radius = a_size * u_ratio;',
// Scale from [[-1 1] [-1 1]] to the container:
'vec2 position = (u_matrix * vec3(a_position, 1)).xy;',
// 'center = (position / u_resolution * 2.0 - 1.0) * vec2(1, -1);',
'center = position * u_scale;',
'center = vec2(center.x, u_scale * u_resolution.y - center.y);',
'position = position +',
'2.0 * radius * vec2(cos(a_angle), sin(a_angle));',
'position = (position / u_resolution * 2.0 - 1.0) * vec2(1, -1);',
'radius = radius * u_scale;',
'gl_Position = vec4(position, 0, 1);',
// Extract the color:
'float c = a_color;',
'color.b = mod(c, 256.0); c = floor(c / 256.0);',
'color.g = mod(c, 256.0); c = floor(c / 256.0);',
'color.r = mod(c, 256.0); c = floor(c / 256.0); color /= 255.0;',
'color.a = 1.0;',
'}'
].join('\n'),
gl.VERTEX_SHADER
);
fragmentShader = sigma.utils.loadShader(
gl,
[
'precision mediump float;',
'varying vec4 color;',
'varying vec2 center;',
'varying float radius;',
'void main(void) {',
'vec4 color0 = vec4(0.0, 0.0, 0.0, 0.0);',
'vec2 m = gl_FragCoord.xy - center;',
'float diff = radius - sqrt(m.x * m.x + m.y * m.y);',
// Here is how we draw a disc instead of a square:
'if (diff > 0.0)',
'gl_FragColor = color;',
'else',
'gl_FragColor = color0;',
'}'
].join('\n'),
gl.FRAGMENT_SHADER
);
program = sigma.utils.loadProgram(gl, [vertexShader, fragmentShader]);
return program;
}
};
})();

View File

@ -0,0 +1,163 @@
;(function() {
'use strict';
sigma.utils.pkg('sigma.webgl.nodes');
/**
* This node renderer will display nodes in the fastest way: Nodes are basic
* squares, drawn through the gl.POINTS drawing method. The size of the nodes
* are represented with the "gl_PointSize" value in the vertex shader.
*
* It is the fastest node renderer here since the buffer just takes one line
* to draw each node (with attributes "x", "y", "size" and "color").
*
* Nevertheless, this method has some problems, especially due to some issues
* with the gl.POINTS:
* - First, if the center of a node is outside the scene, the point will not
* be drawn, even if it should be partly on screen.
* - I tried applying a fragment shader similar to the one in the default
* node renderer to display them as discs, but it did not work fine on
* some computers settings, filling the discs with weird gradients not
* depending on the actual color.
*/
sigma.webgl.nodes.fast = {
POINTS: 1,
ATTRIBUTES: 4,
addNode: function(node, data, i, prefix, settings) {
data[i++] = node[prefix + 'x'];
data[i++] = node[prefix + 'y'];
data[i++] = node[prefix + 'size'];
data[i++] = sigma.utils.floatColor(
node.color || settings('defaultNodeColor')
);
},
render: function(gl, program, data, params) {
var buffer;
// Define attributes:
var positionLocation =
gl.getAttribLocation(program, 'a_position'),
sizeLocation =
gl.getAttribLocation(program, 'a_size'),
colorLocation =
gl.getAttribLocation(program, 'a_color'),
resolutionLocation =
gl.getUniformLocation(program, 'u_resolution'),
matrixLocation =
gl.getUniformLocation(program, 'u_matrix'),
ratioLocation =
gl.getUniformLocation(program, 'u_ratio'),
scaleLocation =
gl.getUniformLocation(program, 'u_scale');
buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW);
gl.uniform2f(resolutionLocation, params.width, params.height);
gl.uniform1f(
ratioLocation,
1 / Math.pow(params.ratio, params.settings('nodesPowRatio'))
);
gl.uniform1f(scaleLocation, params.scalingRatio);
gl.uniformMatrix3fv(matrixLocation, false, params.matrix);
gl.enableVertexAttribArray(positionLocation);
gl.enableVertexAttribArray(sizeLocation);
gl.enableVertexAttribArray(colorLocation);
gl.vertexAttribPointer(
positionLocation,
2,
gl.FLOAT,
false,
this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
0
);
gl.vertexAttribPointer(
sizeLocation,
1,
gl.FLOAT,
false,
this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
8
);
gl.vertexAttribPointer(
colorLocation,
1,
gl.FLOAT,
false,
this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
12
);
gl.drawArrays(
gl.POINTS,
params.start || 0,
params.count || (data.length / this.ATTRIBUTES)
);
},
initProgram: function(gl) {
var vertexShader,
fragmentShader,
program;
vertexShader = sigma.utils.loadShader(
gl,
[
'attribute vec2 a_position;',
'attribute float a_size;',
'attribute float a_color;',
'uniform vec2 u_resolution;',
'uniform float u_ratio;',
'uniform float u_scale;',
'uniform mat3 u_matrix;',
'varying vec4 color;',
'void main() {',
// Scale from [[-1 1] [-1 1]] to the container:
'gl_Position = vec4(',
'((u_matrix * vec3(a_position, 1)).xy /',
'u_resolution * 2.0 - 1.0) * vec2(1, -1),',
'0,',
'1',
');',
// Multiply the point size twice:
// - x SCALING_RATIO to correct the canvas scaling
// - x 2 to correct the formulae
'gl_PointSize = a_size * u_ratio * u_scale * 2.0;',
// Extract the color:
'float c = a_color;',
'color.b = mod(c, 256.0); c = floor(c / 256.0);',
'color.g = mod(c, 256.0); c = floor(c / 256.0);',
'color.r = mod(c, 256.0); c = floor(c / 256.0); color /= 255.0;',
'color.a = 1.0;',
'}'
].join('\n'),
gl.VERTEX_SHADER
);
fragmentShader = sigma.utils.loadShader(
gl,
[
'precision mediump float;',
'varying vec4 color;',
'void main(void) {',
'gl_FragColor = color;',
'}'
].join('\n'),
gl.FRAGMENT_SHADER
);
program = sigma.utils.loadProgram(gl, [vertexShader, fragmentShader]);
return program;
}
};
})();

View File

@ -0,0 +1,739 @@
;(function(undefined) {
'use strict';
var __instances = {};
/**
* This is the sigma instances constructor. One instance of sigma represent
* one graph. It is possible to represent this grapĥ with several renderers
* at the same time. By default, the default renderer (WebGL + Canvas
* polyfill) will be used as the only renderer, with the container specified
* in the configuration.
*
* @param {?*} conf The configuration of the instance. There are a lot of
* different recognized forms to instantiate sigma, check
* example files, documentation in this file and unit
* tests to know more.
* @return {sigma} The fresh new sigma instance.
*
* Instanciating sigma:
* ********************
* If no parameter is given to the constructor, the instance will be created
* without any renderer or camera. It will just instantiate the graph, and
* other modules will have to be instantiated through the public methods,
* like "addRenderer" etc:
*
* > s0 = new sigma();
* > s0.addRenderer({
* > type: 'canvas',
* > container: 'my-container-id'
* > });
*
* In most of the cases, sigma will simply be used with the default renderer.
* Then, since the only required parameter is the DOM container, there are
* some simpler way to call the constructor. The four following calls do the
* exact same things:
*
* > s1 = new sigma('my-container-id');
* > s2 = new sigma(document.getElementById('my-container-id'));
* > s3 = new sigma({
* > container: document.getElementById('my-container-id')
* > });
* > s4 = new sigma({
* > renderers: [{
* > container: document.getElementById('my-container-id')
* > }]
* > });
*
* Recognized parameters:
* **********************
* Here is the exhaustive list of every accepted parameters, when calling the
* constructor with to top level configuration object (fourth case in the
* previous examples):
*
* {?string} id The id of the instance. It will be generated
* automatically if not specified.
* {?array} renderers An array containing objects describing renderers.
* {?object} graph An object containing an array of nodes and an array
* of edges, to avoid having to add them by hand later.
* {?object} settings An object containing instance specific settings that
* will override the default ones defined in the object
* sigma.settings.
*/
var sigma = function(conf) {
// Local variables:
// ****************
var i,
l,
a,
c,
o,
id;
sigma.classes.dispatcher.extend(this);
// Private attributes:
// *******************
var _self = this,
_conf = conf || {};
// Little shortcut:
// ****************
// The configuration is supposed to have a list of the configuration
// objects for each renderer.
// - If there are no configuration at all, then nothing is done.
// - If there are no renderer list, the given configuration object will be
// considered as describing the first and only renderer.
// - If there are no renderer list nor "container" object, it will be
// considered as the container itself (a DOM element).
// - If the argument passed to sigma() is a string, it will be considered
// as the ID of the DOM container.
if (
typeof _conf === 'string' ||
_conf instanceof HTMLElement
)
_conf = {
renderers: [_conf]
};
else if (Object.prototype.toString.call(_conf) === '[object Array]')
_conf = {
renderers: _conf
};
// Also check "renderer" and "container" keys:
o = _conf.renderers || _conf.renderer || _conf.container;
if (!_conf.renderers || _conf.renderers.length === 0)
if (
typeof o === 'string' ||
o instanceof HTMLElement ||
(typeof o === 'object' && 'container' in o)
)
_conf.renderers = [o];
// Recense the instance:
if (_conf.id) {
if (__instances[_conf.id])
throw 'sigma: Instance "' + _conf.id + '" already exists.';
Object.defineProperty(this, 'id', {
value: _conf.id
});
} else {
id = 0;
while (__instances[id])
id++;
Object.defineProperty(this, 'id', {
value: '' + id
});
}
__instances[this.id] = this;
// Initialize settings function:
this.settings = new sigma.classes.configurable(
sigma.settings,
_conf.settings || {}
);
// Initialize locked attributes:
Object.defineProperty(this, 'graph', {
value: new sigma.classes.graph(this.settings),
configurable: true
});
Object.defineProperty(this, 'middlewares', {
value: [],
configurable: true
});
Object.defineProperty(this, 'cameras', {
value: {},
configurable: true
});
Object.defineProperty(this, 'renderers', {
value: {},
configurable: true
});
Object.defineProperty(this, 'renderersPerCamera', {
value: {},
configurable: true
});
Object.defineProperty(this, 'cameraFrames', {
value: {},
configurable: true
});
Object.defineProperty(this, 'camera', {
get: function() {
return this.cameras[0];
}
});
Object.defineProperty(this, 'events', {
value: [
'click',
'rightClick',
'clickStage',
'doubleClickStage',
'rightClickStage',
'clickNode',
'clickNodes',
'doubleClickNode',
'doubleClickNodes',
'rightClickNode',
'rightClickNodes',
'overNode',
'overNodes',
'outNode',
'outNodes',
'downNode',
'downNodes',
'upNode',
'upNodes'
],
configurable: true
});
// Add a custom handler, to redispatch events from renderers:
this._handler = (function(e) {
var k,
data = {};
for (k in e.data)
data[k] = e.data[k];
data.renderer = e.target;
this.dispatchEvent(e.type, data);
}).bind(this);
// Initialize renderers:
a = _conf.renderers || [];
for (i = 0, l = a.length; i < l; i++)
this.addRenderer(a[i]);
// Initialize middlewares:
a = _conf.middlewares || [];
for (i = 0, l = a.length; i < l; i++)
this.middlewares.push(
typeof a[i] === 'string' ?
sigma.middlewares[a[i]] :
a[i]
);
// Check if there is already a graph to fill in:
if (typeof _conf.graph === 'object' && _conf.graph) {
this.graph.read(_conf.graph);
// If a graph is given to the to the instance, the "refresh" method is
// directly called:
this.refresh();
}
// Deal with resize:
window.addEventListener('resize', function() {
if (_self.settings)
_self.refresh();
});
};
/**
* This methods will instantiate and reference a new camera. If no id is
* specified, then an automatic id will be generated.
*
* @param {?string} id Eventually the camera id.
* @return {sigma.classes.camera} The fresh new camera instance.
*/
sigma.prototype.addCamera = function(id) {
var self = this,
camera;
if (!arguments.length) {
id = 0;
while (this.cameras['' + id])
id++;
id = '' + id;
}
if (this.cameras[id])
throw 'sigma.addCamera: The camera "' + id + '" already exists.';
camera = new sigma.classes.camera(id, this.graph, this.settings);
this.cameras[id] = camera;
// Add a quadtree to the camera:
camera.quadtree = new sigma.classes.quad();
// Add an edgequadtree to the camera:
if (sigma.classes.edgequad !== undefined) {
camera.edgequadtree = new sigma.classes.edgequad();
}
camera.bind('coordinatesUpdated', function(e) {
self.renderCamera(camera, camera.isAnimated);
});
this.renderersPerCamera[id] = [];
return camera;
};
/**
* This method kills a camera, and every renderer attached to it.
*
* @param {string|camera} v The camera to kill or its ID.
* @return {sigma} Returns the instance.
*/
sigma.prototype.killCamera = function(v) {
v = typeof v === 'string' ? this.cameras[v] : v;
if (!v)
throw 'sigma.killCamera: The camera is undefined.';
var i,
l,
a = this.renderersPerCamera[v.id];
for (l = a.length, i = l - 1; i >= 0; i--)
this.killRenderer(a[i]);
delete this.renderersPerCamera[v.id];
delete this.cameraFrames[v.id];
delete this.cameras[v.id];
if (v.kill)
v.kill();
return this;
};
/**
* This methods will instantiate and reference a new renderer. The "type"
* argument can be the constructor or its name in the "sigma.renderers"
* package. If no type is specified, then "sigma.renderers.def" will be used.
* If no id is specified, then an automatic id will be generated.
*
* @param {?object} options Eventually some options to give to the renderer
* constructor.
* @return {renderer} The fresh new renderer instance.
*
* Recognized parameters:
* **********************
* Here is the exhaustive list of every accepted parameters in the "options"
* object:
*
* {?string} id Eventually the renderer id.
* {?(function|string)} type Eventually the renderer constructor or its
* name in the "sigma.renderers" package.
* {?(camera|string)} camera Eventually the renderer camera or its
* id.
*/
sigma.prototype.addRenderer = function(options) {
var id,
fn,
camera,
renderer,
o = options || {};
// Polymorphism:
if (typeof o === 'string')
o = {
container: document.getElementById(o)
};
else if (o instanceof HTMLElement)
o = {
container: o
};
// If the container still is a string, we get it by id
if (typeof o.container === 'string')
o.container = document.getElementById(o.container);
// Reference the new renderer:
if (!('id' in o)) {
id = 0;
while (this.renderers['' + id])
id++;
id = '' + id;
} else
id = o.id;
if (this.renderers[id])
throw 'sigma.addRenderer: The renderer "' + id + '" already exists.';
// Find the good constructor:
fn = typeof o.type === 'function' ? o.type : sigma.renderers[o.type];
fn = fn || sigma.renderers.def;
// Find the good camera:
camera = 'camera' in o ?
(
o.camera instanceof sigma.classes.camera ?
o.camera :
this.cameras[o.camera] || this.addCamera(o.camera)
) :
this.addCamera();
if (this.cameras[camera.id] !== camera)
throw 'sigma.addRenderer: The camera is not properly referenced.';
// Instantiate:
renderer = new fn(this.graph, camera, this.settings, o);
this.renderers[id] = renderer;
Object.defineProperty(renderer, 'id', {
value: id
});
// Bind events:
if (renderer.bind)
renderer.bind(
[
'click',
'rightClick',
'clickStage',
'doubleClickStage',
'rightClickStage',
'clickNode',
'clickNodes',
'clickEdge',
'clickEdges',
'doubleClickNode',
'doubleClickNodes',
'doubleClickEdge',
'doubleClickEdges',
'rightClickNode',
'rightClickNodes',
'rightClickEdge',
'rightClickEdges',
'overNode',
'overNodes',
'overEdge',
'overEdges',
'outNode',
'outNodes',
'outEdge',
'outEdges',
'downNode',
'downNodes',
'downEdge',
'downEdges',
'upNode',
'upNodes',
'upEdge',
'upEdges'
],
this._handler
);
// Reference the renderer by its camera:
this.renderersPerCamera[camera.id].push(renderer);
return renderer;
};
/**
* This method kills a renderer.
*
* @param {string|renderer} v The renderer to kill or its ID.
* @return {sigma} Returns the instance.
*/
sigma.prototype.killRenderer = function(v) {
v = typeof v === 'string' ? this.renderers[v] : v;
if (!v)
throw 'sigma.killRenderer: The renderer is undefined.';
var a = this.renderersPerCamera[v.camera.id],
i = a.indexOf(v);
if (i >= 0)
a.splice(i, 1);
if (v.kill)
v.kill();
delete this.renderers[v.id];
return this;
};
/**
* This method calls the "render" method of each renderer, with the same
* arguments than the "render" method, but will also check if the renderer
* has a "process" method, and call it if it exists.
*
* It is useful for quadtrees or WebGL processing, for instance.
*
* @param {?object} options Eventually some options to give to the refresh
* method.
* @return {sigma} Returns the instance itself.
*
* Recognized parameters:
* **********************
* Here is the exhaustive list of every accepted parameters in the "options"
* object:
*
* {?boolean} skipIndexation A flag specifying wether or not the refresh
* function should reindex the graph in the
* quadtrees or not (default: false).
*/
sigma.prototype.refresh = function(options) {
var i,
l,
k,
a,
c,
bounds,
prefix = 0;
options = options || {};
// Call each middleware:
a = this.middlewares || [];
for (i = 0, l = a.length; i < l; i++)
a[i].call(
this,
(i === 0) ? '' : 'tmp' + prefix + ':',
(i === l - 1) ? 'ready:' : ('tmp' + (++prefix) + ':')
);
// Then, for each camera, call the "rescale" middleware, unless the
// settings specify not to:
for (k in this.cameras) {
c = this.cameras[k];
if (
c.settings('autoRescale') &&
this.renderersPerCamera[c.id] &&
this.renderersPerCamera[c.id].length
)
sigma.middlewares.rescale.call(
this,
a.length ? 'ready:' : '',
c.readPrefix,
{
width: this.renderersPerCamera[c.id][0].width,
height: this.renderersPerCamera[c.id][0].height
}
);
else
sigma.middlewares.copy.call(
this,
a.length ? 'ready:' : '',
c.readPrefix
);
if (!options.skipIndexation) {
// Find graph boundaries:
bounds = sigma.utils.getBoundaries(
this.graph,
c.readPrefix
);
// Refresh quadtree:
c.quadtree.index(this.graph.nodes(), {
prefix: c.readPrefix,
bounds: {
x: bounds.minX,
y: bounds.minY,
width: bounds.maxX - bounds.minX,
height: bounds.maxY - bounds.minY
}
});
// Refresh edgequadtree:
if (
c.edgequadtree !== undefined &&
c.settings('drawEdges') &&
c.settings('enableEdgeHovering')
) {
c.edgequadtree.index(this.graph, {
prefix: c.readPrefix,
bounds: {
x: bounds.minX,
y: bounds.minY,
width: bounds.maxX - bounds.minX,
height: bounds.maxY - bounds.minY
}
});
}
}
}
// Call each renderer:
a = Object.keys(this.renderers);
for (i = 0, l = a.length; i < l; i++)
if (this.renderers[a[i]].process) {
if (this.settings('skipErrors'))
try {
this.renderers[a[i]].process();
} catch (e) {
console.log(
'Warning: The renderer "' + a[i] + '" crashed on ".process()"'
);
}
else
this.renderers[a[i]].process();
}
this.render();
return this;
};
/**
* This method calls the "render" method of each renderer.
*
* @return {sigma} Returns the instance itself.
*/
sigma.prototype.render = function() {
var i,
l,
a,
prefix = 0;
// Call each renderer:
a = Object.keys(this.renderers);
for (i = 0, l = a.length; i < l; i++)
if (this.settings('skipErrors'))
try {
this.renderers[a[i]].render();
} catch (e) {
if (this.settings('verbose'))
console.log(
'Warning: The renderer "' + a[i] + '" crashed on ".render()"'
);
}
else
this.renderers[a[i]].render();
return this;
};
/**
* This method calls the "render" method of each renderer that is bound to
* the specified camera. To improve the performances, if this method is
* called too often, the number of effective renderings is limitated to one
* per frame, unless you are using the "force" flag.
*
* @param {sigma.classes.camera} camera The camera to render.
* @param {?boolean} force If true, will render the camera
* directly.
* @return {sigma} Returns the instance itself.
*/
sigma.prototype.renderCamera = function(camera, force) {
var i,
l,
a,
self = this;
if (force) {
a = this.renderersPerCamera[camera.id];
for (i = 0, l = a.length; i < l; i++)
if (this.settings('skipErrors'))
try {
a[i].render();
} catch (e) {
if (this.settings('verbose'))
console.log(
'Warning: The renderer "' + a[i].id + '" crashed on ".render()"'
);
}
else
a[i].render();
} else {
if (!this.cameraFrames[camera.id]) {
a = this.renderersPerCamera[camera.id];
for (i = 0, l = a.length; i < l; i++)
if (this.settings('skipErrors'))
try {
a[i].render();
} catch (e) {
if (this.settings('verbose'))
console.log(
'Warning: The renderer "' +
a[i].id +
'" crashed on ".render()"'
);
}
else
a[i].render();
this.cameraFrames[camera.id] = requestAnimationFrame(function() {
delete self.cameraFrames[camera.id];
});
}
}
return this;
};
/**
* This method calls the "kill" method of each module and destroys any
* reference from the instance.
*/
sigma.prototype.kill = function() {
var k;
// Dispatching event
this.dispatchEvent('kill');
// Kill graph:
this.graph.kill();
// Kill middlewares:
delete this.middlewares;
// Kill each renderer:
for (k in this.renderers)
this.killRenderer(this.renderers[k]);
// Kill each camera:
for (k in this.cameras)
this.killCamera(this.cameras[k]);
delete this.renderers;
delete this.cameras;
// Kill everything else:
for (k in this)
if (this.hasOwnProperty(k))
delete this[k];
delete __instances[this.id];
};
/**
* Returns a clone of the instances object or a specific running instance.
*
* @param {?string} id Eventually an instance ID.
* @return {object} The related instance or a clone of the instances
* object.
*/
sigma.instances = function(id) {
return arguments.length ?
__instances[id] :
sigma.utils.extend({}, __instances);
};
/**
* The current version of sigma:
*/
sigma.version = '1.0.3';
/**
* EXPORT:
* *******
*/
if (typeof this.sigma !== 'undefined')
throw 'An object called sigma is already in the global scope.';
this.sigma = sigma;
}).call(this);

View File

@ -0,0 +1,20 @@
// Hardcoded export for the node.js version:
var sigma = this.sigma,
conrad = this.conrad;
sigma.conrad = conrad;
// Dirty polyfills to permit sigma usage in node
if (HTMLElement === undefined)
var HTMLElement = function() {};
if (window === undefined)
var window = {
addEventListener: function() {}
};
if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports)
exports = module.exports = sigma;
exports.sigma = sigma;
}

View File

@ -0,0 +1,251 @@
;(function(undefined) {
'use strict';
if (typeof sigma === 'undefined')
throw 'sigma is not declared';
// Packages initialization:
sigma.utils.pkg('sigma.settings');
var settings = {
/**
* GRAPH SETTINGS:
* ***************
*/
// {boolean} Indicates if the data have to be cloned in methods to add
// nodes or edges.
clone: true,
// {boolean} Indicates if nodes "id" values and edges "id", "source" and
// "target" values must be set as immutable.
immutable: true,
// {boolean} Indicates if sigma can log its errors and warnings.
verbose: false,
/**
* RENDERERS SETTINGS:
* *******************
*/
// {string}
classPrefix: 'sigma',
// {string}
defaultNodeType: 'def',
// {string}
defaultEdgeType: 'def',
// {string}
defaultLabelColor: '#000',
// {string}
defaultEdgeColor: '#000',
// {string}
defaultNodeColor: '#000',
// {string}
defaultLabelSize: 14,
// {string} Indicates how to choose the edges color. Available values:
// "source", "target", "default"
edgeColor: 'source',
// {number} Defines the minimal edge's arrow display size.
minArrowSize: 0,
// {string}
font: 'arial',
// {string} Example: 'bold'
fontStyle: '',
// {string} Indicates how to choose the labels color. Available values:
// "node", "default"
labelColor: 'default',
// {string} Indicates how to choose the labels size. Available values:
// "fixed", "proportional"
labelSize: 'fixed',
// {string} The ratio between the font size of the label and the node size.
labelSizeRatio: 1,
// {number} The minimum size a node must have to see its label displayed.
labelThreshold: 8,
// {number} The oversampling factor used in WebGL renderer.
webglOversamplingRatio: 2,
// {number} The size of the border of hovered nodes.
borderSize: 0,
// {number} The default hovered node border's color.
defaultNodeBorderColor: '#000',
// {number} The hovered node's label font. If not specified, will heritate
// the "font" value.
hoverFont: '',
// {boolean} If true, then only one node can be hovered at a time.
singleHover: true,
// {string} Example: 'bold'
hoverFontStyle: '',
// {string} Indicates how to choose the hovered nodes shadow color.
// Available values: "node", "default"
labelHoverShadow: 'default',
// {string}
labelHoverShadowColor: '#000',
// {string} Indicates how to choose the hovered nodes color.
// Available values: "node", "default"
nodeHoverColor: 'node',
// {string}
defaultNodeHoverColor: '#000',
// {string} Indicates how to choose the hovered nodes background color.
// Available values: "node", "default"
labelHoverBGColor: 'default',
// {string}
defaultHoverLabelBGColor: '#fff',
// {string} Indicates how to choose the hovered labels color.
// Available values: "node", "default"
labelHoverColor: 'default',
// {string}
defaultLabelHoverColor: '#000',
// {string} Indicates how to choose the edges hover color. Available values:
// "edge", "default"
edgeHoverColor: 'edge',
// {number} The size multiplicator of hovered edges.
edgeHoverSizeRatio: 1,
// {string}
defaultEdgeHoverColor: '#000',
// {boolean} Indicates if the edge extremities must be hovered when the
// edge is hovered.
edgeHoverExtremities: false,
// {booleans} The different drawing modes:
// false: Layered not displayed.
// true: Layered displayed.
drawEdges: true,
drawNodes: true,
drawLabels: true,
drawEdgeLabels: false,
// {boolean} Indicates if the edges must be drawn in several frames or in
// one frame, as the nodes and labels are drawn.
batchEdgesDrawing: false,
// {boolean} Indicates if the edges must be hidden during dragging and
// animations.
hideEdgesOnMove: false,
// {numbers} The different batch sizes, when elements are displayed in
// several frames.
canvasEdgesBatchSize: 500,
webglEdgesBatchSize: 1000,
/**
* RESCALE SETTINGS:
* *****************
*/
// {string} Indicates of to scale the graph relatively to its container.
// Available values: "inside", "outside"
scalingMode: 'inside',
// {number} The margin to keep around the graph.
sideMargin: 0,
// {number} Determine the size of the smallest and the biggest node / edges
// on the screen. This mapping makes easier to display the graph,
// avoiding too big nodes that take half of the screen, or too
// small ones that are not readable. If the two parameters are
// equals, then the minimal display size will be 0. And if they
// are both equal to 0, then there is no mapping, and the radius
// of the nodes will be their size.
minEdgeSize: 0.5,
maxEdgeSize: 1,
minNodeSize: 1,
maxNodeSize: 8,
/**
* CAPTORS SETTINGS:
* *****************
*/
// {boolean}
touchEnabled: true,
// {boolean}
mouseEnabled: true,
// {boolean}
mouseWheelEnabled: true,
// {boolean}
doubleClickEnabled: true,
// {boolean} Defines whether the custom events such as "clickNode" can be
// used.
eventsEnabled: true,
// {number} Defines by how much multiplicating the zooming level when the
// user zooms with the mouse-wheel.
zoomingRatio: 1.7,
// {number} Defines by how much multiplicating the zooming level when the
// user zooms by double clicking.
doubleClickZoomingRatio: 2.2,
// {number} The minimum zooming level.
zoomMin: 0.0625,
// {number} The maximum zooming level.
zoomMax: 2,
// {number} The duration of animations following a mouse scrolling.
mouseZoomDuration: 200,
// {number} The duration of animations following a mouse double click.
doubleClickZoomDuration: 200,
// {number} The duration of animations following a mouse dropping.
mouseInertiaDuration: 200,
// {number} The inertia power (mouse captor).
mouseInertiaRatio: 3,
// {number} The duration of animations following a touch dropping.
touchInertiaDuration: 200,
// {number} The inertia power (touch captor).
touchInertiaRatio: 3,
// {number} The maximum time between two clicks to make it a double click.
doubleClickTimeout: 300,
// {number} The maximum time between two taps to make it a double tap.
doubleTapTimeout: 300,
// {number} The maximum time of dragging to trigger intertia.
dragTimeout: 200,
/**
* GLOBAL SETTINGS:
* ****************
*/
// {boolean} Determines whether the instance has to refresh itself
// automatically when a "resize" event is dispatched from the
// window object.
autoResize: true,
// {boolean} Determines whether the "rescale" middleware has to be called
// automatically for each camera on refresh.
autoRescale: true,
// {boolean} If set to false, the camera method "goTo" will basically do
// nothing.
enableCamera: true,
// {boolean} If set to false, the nodes cannot be hovered.
enableHovering: true,
// {boolean} If set to true, the edges can be hovered.
enableEdgeHovering: false,
// {number} The size of the area around the edges to activate hovering.
edgeHoverPrecision: 5,
// {boolean} If set to true, the rescale middleware will ignore node sizes
// to determine the graphs boundings.
rescaleIgnoreSize: false,
// {boolean} Determines if the core has to try to catch errors on
// rendering.
skipErrors: false,
/**
* CAMERA SETTINGS:
* ****************
*/
// {number} The power degrees applied to the nodes/edges size relatively to
// the zooming level. Basically:
// > onScreenR = Math.pow(zoom, nodesPowRatio) * R
// > onScreenT = Math.pow(zoom, edgesPowRatio) * T
nodesPowRatio: 0.5,
edgesPowRatio: 0.5,
/**
* ANIMATIONS SETTINGS:
* ********************
*/
// {number} The default animation time.
animationsTime: 200
};
// Export the previously designed settings:
sigma.settings = sigma.utils.extend(sigma.settings || {}, settings);
}).call(this);

View File

@ -0,0 +1,77 @@
;(function(global) {
'use strict';
/**
* http://paulirish.com/2011/requestanimationframe-for-smart-animating/
* http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
* requestAnimationFrame polyfill by Erik Möller.
* fixes from Paul Irish and Tino Zijdel
* MIT license
*/
var x,
lastTime = 0,
vendors = ['ms', 'moz', 'webkit', 'o'];
for (x = 0; x < vendors.length && !global.requestAnimationFrame; x++) {
global.requestAnimationFrame =
global[vendors[x] + 'RequestAnimationFrame'];
global.cancelAnimationFrame =
global[vendors[x] + 'CancelAnimationFrame'] ||
global[vendors[x] + 'CancelRequestAnimationFrame'];
}
if (!global.requestAnimationFrame)
global.requestAnimationFrame = function(callback, element) {
var currTime = new Date().getTime(),
timeToCall = Math.max(0, 16 - (currTime - lastTime)),
id = global.setTimeout(
function() {
callback(currTime + timeToCall);
},
timeToCall
);
lastTime = currTime + timeToCall;
return id;
};
if (!global.cancelAnimationFrame)
global.cancelAnimationFrame = function(id) {
clearTimeout(id);
};
/**
* Function.prototype.bind polyfill found on MDN.
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind#Compatibility
* Public domain
*/
if (!Function.prototype.bind)
Function.prototype.bind = function(oThis) {
if (typeof this !== 'function')
// Closest thing possible to the ECMAScript 5 internal IsCallable
// function:
throw new TypeError(
'Function.prototype.bind - what is trying to be bound is not callable'
);
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP,
fBound;
fNOP = function() {};
fBound = function() {
return fToBind.apply(
this instanceof fNOP && oThis ?
this :
oThis,
aArgs.concat(Array.prototype.slice.call(arguments))
);
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
})(this);

View File

@ -0,0 +1,950 @@
;(function(undefined) {
'use strict';
if (typeof sigma === 'undefined')
throw 'sigma is not declared';
var _root = this;
// Initialize packages:
sigma.utils = sigma.utils || {};
/**
* MISC UTILS:
*/
/**
* This function takes any number of objects as arguments, copies from each
* of these objects each pair key/value into a new object, and finally
* returns this object.
*
* The arguments are parsed from the last one to the first one, such that
* when several objects have keys in common, the "earliest" object wins.
*
* Example:
* ********
* > var o1 = {
* > a: 1,
* > b: 2,
* > c: '3'
* > },
* > o2 = {
* > c: '4',
* > d: [ 5 ]
* > };
* > sigma.utils.extend(o1, o2);
* > // Returns: {
* > // a: 1,
* > // b: 2,
* > // c: '3',
* > // d: [ 5 ]
* > // };
*
* @param {object+} Any number of objects.
* @return {object} The merged object.
*/
sigma.utils.extend = function() {
var i,
k,
res = {},
l = arguments.length;
for (i = l - 1; i >= 0; i--)
for (k in arguments[i])
res[k] = arguments[i][k];
return res;
};
/**
* A short "Date.now()" polyfill.
*
* @return {Number} The current time (in ms).
*/
sigma.utils.dateNow = function() {
return Date.now ? Date.now() : new Date().getTime();
};
/**
* Takes a package name as parameter and checks at each lebel if it exists,
* and if it does not, creates it.
*
* Example:
* ********
* > sigma.utils.pkg('a.b.c');
* > a.b.c;
* > // Object {};
* >
* > sigma.utils.pkg('a.b.d');
* > a.b;
* > // Object { c: {}, d: {} };
*
* @param {string} pkgName The name of the package to create/find.
* @return {object} The related package.
*/
sigma.utils.pkg = function(pkgName) {
return (pkgName || '').split('.').reduce(function(context, objName) {
return (objName in context) ?
context[objName] :
(context[objName] = {});
}, _root);
};
/**
* Returns a unique incremental number ID.
*
* Example:
* ********
* > sigma.utils.id();
* > // 1;
* >
* > sigma.utils.id();
* > // 2;
* >
* > sigma.utils.id();
* > // 3;
*
* @param {string} pkgName The name of the package to create/find.
* @return {object} The related package.
*/
sigma.utils.id = (function() {
var i = 0;
return function() {
return ++i;
};
})();
/**
* This function takes an hexa color (for instance "#ffcc00" or "#fc0") or a
* rgb / rgba color (like "rgb(255,255,12)" or "rgba(255,255,12,1)") and
* returns an integer equal to "r * 255 * 255 + g * 255 + b", to gain some
* memory in the data given to WebGL shaders.
*
* @param {string} val The hexa or rgba color.
* @return {number} The number value.
*/
sigma.utils.floatColor = function(val) {
var result = [0, 0, 0];
if (val.match(/^#/)) {
val = (val || '').replace(/^#/, '');
result = (val.length === 3) ?
[
parseInt(val.charAt(0) + val.charAt(0), 16),
parseInt(val.charAt(1) + val.charAt(1), 16),
parseInt(val.charAt(2) + val.charAt(2), 16)
] :
[
parseInt(val.charAt(0) + val.charAt(1), 16),
parseInt(val.charAt(2) + val.charAt(3), 16),
parseInt(val.charAt(4) + val.charAt(5), 16)
];
} else if (val.match(/^ *rgba? *\(/)) {
val = val.match(
/^ *rgba? *\( *([0-9]*) *, *([0-9]*) *, *([0-9]*) *(,.*)?\) *$/
);
result = [
+val[1],
+val[2],
+val[3]
];
}
return (
result[0] * 256 * 256 +
result[1] * 256 +
result[2]
);
};
/**
* Perform a zoom into a camera, with or without animation, to the
* coordinates indicated using a specified ratio.
*
* Recognized parameters:
* **********************
* Here is the exhaustive list of every accepted parameters in the animation
* object:
*
* {?number} duration An amount of time that means the duration of the
* animation. If this parameter doesn't exist the
* zoom will be performed without animation.
* {?function} onComplete A function to perform it after the animation. It
* will be performed even if there is no duration.
*
* @param {camera} The camera where perform the zoom.
* @param {x} The X coordiantion where the zoom goes.
* @param {y} The Y coordiantion where the zoom goes.
* @param {ratio} The ratio to apply it to the current camera ratio.
* @param {?animation} A dictionary with options for a possible animation.
*/
sigma.utils.zoomTo = function(camera, x, y, ratio, animation) {
var settings = camera.settings,
count,
newRatio,
animationSettings,
coordinates;
// Create the newRatio dealing with min / max:
newRatio = Math.max(
settings('zoomMin'),
Math.min(
settings('zoomMax'),
camera.ratio * ratio
)
);
// Check that the new ratio is different from the initial one:
if (newRatio !== camera.ratio) {
// Create the coordinates variable:
ratio = newRatio / camera.ratio;
coordinates = {
x: x * (1 - ratio) + camera.x,
y: y * (1 - ratio) + camera.y,
ratio: newRatio
};
if (animation && animation.duration) {
// Complete the animation setings:
count = sigma.misc.animation.killAll(camera);
animation = sigma.utils.extend(
animation,
{
easing: count ? 'quadraticOut' : 'quadraticInOut'
}
);
sigma.misc.animation.camera(camera, coordinates, animation);
} else {
camera.goTo(coordinates);
if (animation && animation.onComplete)
animation.onComplete();
}
}
};
/**
* Return the control point coordinates for a quadratic bezier curve.
*
* @param {number} x1 The X coordinate of the start point.
* @param {number} y1 The Y coordinate of the start point.
* @param {number} x2 The X coordinate of the end point.
* @param {number} y2 The Y coordinate of the end point.
* @return {x,y} The control point coordinates.
*/
sigma.utils.getQuadraticControlPoint = function(x1, y1, x2, y2) {
return {
x: (x1 + x2) / 2 + (y2 - y1) / 4,
y: (y1 + y2) / 2 + (x1 - x2) / 4
};
};
/**
* Compute the coordinates of the point positioned
* at length t in the quadratic bezier curve.
*
* @param {number} t In [0,1] the step percentage to reach
* the point in the curve from the context point.
* @param {number} x1 The X coordinate of the context point.
* @param {number} y1 The Y coordinate of the context point.
* @param {number} x2 The X coordinate of the ending point.
* @param {number} y2 The Y coordinate of the ending point.
* @param {number} xi The X coordinate of the control point.
* @param {number} yi The Y coordinate of the control point.
* @return {object} {x,y}.
*/
sigma.utils.getPointOnQuadraticCurve = function(t, x1, y1, x2, y2, xi, yi) {
// http://stackoverflow.com/a/5634528
return {
x: Math.pow(1 - t, 2) * x1 + 2 * (1 - t) * t * xi + Math.pow(t, 2) * x2,
y: Math.pow(1 - t, 2) * y1 + 2 * (1 - t) * t * yi + Math.pow(t, 2) * y2
};
};
/**
* Compute the coordinates of the point positioned
* at length t in the cubic bezier curve.
*
* @param {number} t In [0,1] the step percentage to reach
* the point in the curve from the context point.
* @param {number} x1 The X coordinate of the context point.
* @param {number} y1 The Y coordinate of the context point.
* @param {number} x2 The X coordinate of the end point.
* @param {number} y2 The Y coordinate of the end point.
* @param {number} cx The X coordinate of the first control point.
* @param {number} cy The Y coordinate of the first control point.
* @param {number} dx The X coordinate of the second control point.
* @param {number} dy The Y coordinate of the second control point.
* @return {object} {x,y} The point at t.
*/
sigma.utils.getPointOnBezierCurve =
function(t, x1, y1, x2, y2, cx, cy, dx, dy) {
// http://stackoverflow.com/a/15397596
// Blending functions:
var B0_t = Math.pow(1 - t, 3),
B1_t = 3 * t * Math.pow(1 - t, 2),
B2_t = 3 * Math.pow(t, 2) * (1 - t),
B3_t = Math.pow(t, 3);
return {
x: (B0_t * x1) + (B1_t * cx) + (B2_t * dx) + (B3_t * x2),
y: (B0_t * y1) + (B1_t * cy) + (B2_t * dy) + (B3_t * y2)
};
};
/**
* Return the coordinates of the two control points for a self loop (i.e.
* where the start point is also the end point) computed as a cubic bezier
* curve.
*
* @param {number} x The X coordinate of the node.
* @param {number} y The Y coordinate of the node.
* @param {number} size The node size.
* @return {x1,y1,x2,y2} The coordinates of the two control points.
*/
sigma.utils.getSelfLoopControlPoints = function(x , y, size) {
return {
x1: x - size * 7,
y1: y,
x2: x,
y2: y + size * 7
};
};
/**
* Return the euclidian distance between two points of a plane
* with an orthonormal basis.
*
* @param {number} x1 The X coordinate of the first point.
* @param {number} y1 The Y coordinate of the first point.
* @param {number} x2 The X coordinate of the second point.
* @param {number} y2 The Y coordinate of the second point.
* @return {number} The euclidian distance.
*/
sigma.utils.getDistance = function(x0, y0, x1, y1) {
return Math.sqrt(Math.pow(x1 - x0, 2) + Math.pow(y1 - y0, 2));
};
/**
* Return the coordinates of the intersection points of two circles.
*
* @param {number} x0 The X coordinate of center location of the first
* circle.
* @param {number} y0 The Y coordinate of center location of the first
* circle.
* @param {number} r0 The radius of the first circle.
* @param {number} x1 The X coordinate of center location of the second
* circle.
* @param {number} y1 The Y coordinate of center location of the second
* circle.
* @param {number} r1 The radius of the second circle.
* @return {xi,yi} The coordinates of the intersection points.
*/
sigma.utils.getCircleIntersection = function(x0, y0, r0, x1, y1, r1) {
// http://stackoverflow.com/a/12219802
var a, dx, dy, d, h, rx, ry, x2, y2;
// dx and dy are the vertical and horizontal distances between the circle
// centers:
dx = x1 - x0;
dy = y1 - y0;
// Determine the straight-line distance between the centers:
d = Math.sqrt((dy * dy) + (dx * dx));
// Check for solvability:
if (d > (r0 + r1)) {
// No solution. circles do not intersect.
return false;
}
if (d < Math.abs(r0 - r1)) {
// No solution. one circle is contained in the other.
return false;
}
//'point 2' is the point where the line through the circle intersection
// points crosses the line between the circle centers.
// Determine the distance from point 0 to point 2:
a = ((r0 * r0) - (r1 * r1) + (d * d)) / (2.0 * d);
// Determine the coordinates of point 2:
x2 = x0 + (dx * a / d);
y2 = y0 + (dy * a / d);
// Determine the distance from point 2 to either of the intersection
// points:
h = Math.sqrt((r0 * r0) - (a * a));
// Determine the offsets of the intersection points from point 2:
rx = -dy * (h / d);
ry = dx * (h / d);
// Determine the absolute intersection points:
var xi = x2 + rx;
var xi_prime = x2 - rx;
var yi = y2 + ry;
var yi_prime = y2 - ry;
return {xi: xi, xi_prime: xi_prime, yi: yi, yi_prime: yi_prime};
};
/**
* Check if a point is on a line segment.
*
* @param {number} x The X coordinate of the point to check.
* @param {number} y The Y coordinate of the point to check.
* @param {number} x1 The X coordinate of the line start point.
* @param {number} y1 The Y coordinate of the line start point.
* @param {number} x2 The X coordinate of the line end point.
* @param {number} y2 The Y coordinate of the line end point.
* @param {number} epsilon The precision (consider the line thickness).
* @return {boolean} True if point is "close to" the line
* segment, false otherwise.
*/
sigma.utils.isPointOnSegment = function(x, y, x1, y1, x2, y2, epsilon) {
// http://stackoverflow.com/a/328122
var crossProduct = Math.abs((y - y1) * (x2 - x1) - (x - x1) * (y2 - y1)),
d = sigma.utils.getDistance(x1, y1, x2, y2),
nCrossProduct = crossProduct / d; // normalized cross product
return (nCrossProduct < epsilon &&
Math.min(x1, x2) <= x && x <= Math.max(x1, x2) &&
Math.min(y1, y2) <= y && y <= Math.max(y1, y2));
};
/**
* Check if a point is on a quadratic bezier curve segment with a thickness.
*
* @param {number} x The X coordinate of the point to check.
* @param {number} y The Y coordinate of the point to check.
* @param {number} x1 The X coordinate of the curve start point.
* @param {number} y1 The Y coordinate of the curve start point.
* @param {number} x2 The X coordinate of the curve end point.
* @param {number} y2 The Y coordinate of the curve end point.
* @param {number} cpx The X coordinate of the curve control point.
* @param {number} cpy The Y coordinate of the curve control point.
* @param {number} epsilon The precision (consider the line thickness).
* @return {boolean} True if (x,y) is on the curve segment,
* false otherwise.
*/
sigma.utils.isPointOnQuadraticCurve =
function(x, y, x1, y1, x2, y2, cpx, cpy, epsilon) {
// Fails if the point is too far from the extremities of the segment,
// preventing for more costly computation:
var dP1P2 = sigma.utils.getDistance(x1, y1, x2, y2);
if (Math.abs(x - x1) > dP1P2 || Math.abs(y - y1) > dP1P2) {
return false;
}
var dP1 = sigma.utils.getDistance(x, y, x1, y1),
dP2 = sigma.utils.getDistance(x, y, x2, y2),
t = 0.5,
r = (dP1 < dP2) ? -0.01 : 0.01,
rThreshold = 0.001,
i = 100,
pt = sigma.utils.getPointOnQuadraticCurve(t, x1, y1, x2, y2, cpx, cpy),
dt = sigma.utils.getDistance(x, y, pt.x, pt.y),
old_dt;
// This algorithm minimizes the distance from the point to the curve. It
// find the optimal t value where t=0 is the start point and t=1 is the end
// point of the curve, starting from t=0.5.
// It terminates because it runs a maximum of i interations.
while (i-- > 0 &&
t >= 0 && t <= 1 &&
(dt > epsilon) &&
(r > rThreshold || r < -rThreshold)) {
old_dt = dt;
pt = sigma.utils.getPointOnQuadraticCurve(t, x1, y1, x2, y2, cpx, cpy);
dt = sigma.utils.getDistance(x, y, pt.x, pt.y);
if (dt > old_dt) {
// not the right direction:
// halfstep in the opposite direction
r = -r / 2;
t += r;
}
else if (t + r < 0 || t + r > 1) {
// oops, we've gone too far:
// revert with a halfstep
r = r / 2;
dt = old_dt;
}
else {
// progress:
t += r;
}
}
return dt < epsilon;
};
/**
* Check if a point is on a cubic bezier curve segment with a thickness.
*
* @param {number} x The X coordinate of the point to check.
* @param {number} y The Y coordinate of the point to check.
* @param {number} x1 The X coordinate of the curve start point.
* @param {number} y1 The Y coordinate of the curve start point.
* @param {number} x2 The X coordinate of the curve end point.
* @param {number} y2 The Y coordinate of the curve end point.
* @param {number} cpx1 The X coordinate of the 1st curve control point.
* @param {number} cpy1 The Y coordinate of the 1st curve control point.
* @param {number} cpx2 The X coordinate of the 2nd curve control point.
* @param {number} cpy2 The Y coordinate of the 2nd curve control point.
* @param {number} epsilon The precision (consider the line thickness).
* @return {boolean} True if (x,y) is on the curve segment,
* false otherwise.
*/
sigma.utils.isPointOnBezierCurve =
function(x, y, x1, y1, x2, y2, cpx1, cpy1, cpx2, cpy2, epsilon) {
// Fails if the point is too far from the extremities of the segment,
// preventing for more costly computation:
var dP1CP1 = sigma.utils.getDistance(x1, y1, cpx1, cpy1);
if (Math.abs(x - x1) > dP1CP1 || Math.abs(y - y1) > dP1CP1) {
return false;
}
var dP1 = sigma.utils.getDistance(x, y, x1, y1),
dP2 = sigma.utils.getDistance(x, y, x2, y2),
t = 0.5,
r = (dP1 < dP2) ? -0.01 : 0.01,
rThreshold = 0.001,
i = 100,
pt = sigma.utils.getPointOnBezierCurve(
t, x1, y1, x2, y2, cpx1, cpy1, cpx2, cpy2),
dt = sigma.utils.getDistance(x, y, pt.x, pt.y),
old_dt;
// This algorithm minimizes the distance from the point to the curve. It
// find the optimal t value where t=0 is the start point and t=1 is the end
// point of the curve, starting from t=0.5.
// It terminates because it runs a maximum of i interations.
while (i-- > 0 &&
t >= 0 && t <= 1 &&
(dt > epsilon) &&
(r > rThreshold || r < -rThreshold)) {
old_dt = dt;
pt = sigma.utils.getPointOnBezierCurve(
t, x1, y1, x2, y2, cpx1, cpy1, cpx2, cpy2);
dt = sigma.utils.getDistance(x, y, pt.x, pt.y);
if (dt > old_dt) {
// not the right direction:
// halfstep in the opposite direction
r = -r / 2;
t += r;
}
else if (t + r < 0 || t + r > 1) {
// oops, we've gone too far:
// revert with a halfstep
r = r / 2;
dt = old_dt;
}
else {
// progress:
t += r;
}
}
return dt < epsilon;
};
/**
* ************
* EVENTS UTILS:
* ************
*/
/**
* Here are some useful functions to unify extraction of the information we
* need with mouse events and touch events, from different browsers:
*/
/**
* Extract the local X position from a mouse or touch event.
*
* @param {event} e A mouse or touch event.
* @return {number} The local X value of the mouse.
*/
sigma.utils.getX = function(e) {
return (
(e.offsetX !== undefined && e.offsetX) ||
(e.layerX !== undefined && e.layerX) ||
(e.clientX !== undefined && e.clientX)
);
};
/**
* Extract the local Y position from a mouse or touch event.
*
* @param {event} e A mouse or touch event.
* @return {number} The local Y value of the mouse.
*/
sigma.utils.getY = function(e) {
return (
(e.offsetY !== undefined && e.offsetY) ||
(e.layerY !== undefined && e.layerY) ||
(e.clientY !== undefined && e.clientY)
);
};
/**
* Extract the width from a mouse or touch event.
*
* @param {event} e A mouse or touch event.
* @return {number} The width of the event's target.
*/
sigma.utils.getWidth = function(e) {
var w = (!e.target.ownerSVGElement) ?
e.target.width :
e.target.ownerSVGElement.width;
return (
(typeof w === 'number' && w) ||
(w !== undefined && w.baseVal !== undefined && w.baseVal.value)
);
};
/**
* Extract the height from a mouse or touch event.
*
* @param {event} e A mouse or touch event.
* @return {number} The height of the event's target.
*/
sigma.utils.getHeight = function(e) {
var h = (!e.target.ownerSVGElement) ?
e.target.height :
e.target.ownerSVGElement.height;
return (
(typeof h === 'number' && h) ||
(h !== undefined && h.baseVal !== undefined && h.baseVal.value)
);
};
/**
* Extract the wheel delta from a mouse or touch event.
*
* @param {event} e A mouse or touch event.
* @return {number} The wheel delta of the mouse.
*/
sigma.utils.getDelta = function(e) {
return (
(e.wheelDelta !== undefined && e.wheelDelta) ||
(e.detail !== undefined && -e.detail)
);
};
/**
* Returns the offset of a DOM element.
*
* @param {DOMElement} dom The element to retrieve the position.
* @return {object} The offset of the DOM element (top, left).
*/
sigma.utils.getOffset = function(dom) {
var left = 0,
top = 0;
while (dom) {
top = top + parseInt(dom.offsetTop);
left = left + parseInt(dom.offsetLeft);
dom = dom.offsetParent;
}
return {
top: top,
left: left
};
};
/**
* Simulates a "double click" event.
*
* @param {HTMLElement} target The event target.
* @param {string} type The event type.
* @param {function} callback The callback to execute.
*/
sigma.utils.doubleClick = function(target, type, callback) {
var clicks = 0,
self = this,
handlers;
target._doubleClickHandler = target._doubleClickHandler || {};
target._doubleClickHandler[type] = target._doubleClickHandler[type] || [];
handlers = target._doubleClickHandler[type];
handlers.push(function(e) {
clicks++;
if (clicks === 2) {
clicks = 0;
return callback(e);
} else if (clicks === 1) {
setTimeout(function() {
clicks = 0;
}, sigma.settings.doubleClickTimeout);
}
});
target.addEventListener(type, handlers[handlers.length - 1], false);
};
/**
* Unbind simulated "double click" events.
*
* @param {HTMLElement} target The event target.
* @param {string} type The event type.
*/
sigma.utils.unbindDoubleClick = function(target, type) {
var handler,
handlers = (target._doubleClickHandler || {})[type] || [];
while ((handler = handlers.pop())) {
target.removeEventListener(type, handler);
}
delete (target._doubleClickHandler || {})[type];
};
/**
* Here are just some of the most basic easing functions, used for the
* animated camera "goTo" calls.
*
* If you need some more easings functions, don't hesitate to add them to
* sigma.utils.easings. But I will not add some more here or merge PRs
* containing, because I do not want sigma sources full of overkill and never
* used stuff...
*/
sigma.utils.easings = sigma.utils.easings || {};
sigma.utils.easings.linearNone = function(k) {
return k;
};
sigma.utils.easings.quadraticIn = function(k) {
return k * k;
};
sigma.utils.easings.quadraticOut = function(k) {
return k * (2 - k);
};
sigma.utils.easings.quadraticInOut = function(k) {
if ((k *= 2) < 1)
return 0.5 * k * k;
return - 0.5 * (--k * (k - 2) - 1);
};
sigma.utils.easings.cubicIn = function(k) {
return k * k * k;
};
sigma.utils.easings.cubicOut = function(k) {
return --k * k * k + 1;
};
sigma.utils.easings.cubicInOut = function(k) {
if ((k *= 2) < 1)
return 0.5 * k * k * k;
return 0.5 * ((k -= 2) * k * k + 2);
};
/**
* ************
* WEBGL UTILS:
* ************
*/
/**
* Loads a WebGL shader and returns it.
*
* @param {WebGLContext} gl The WebGLContext to use.
* @param {string} shaderSource The shader source.
* @param {number} shaderType The type of shader.
* @param {function(string): void} error Callback for errors.
* @return {WebGLShader} The created shader.
*/
sigma.utils.loadShader = function(gl, shaderSource, shaderType, error) {
var compiled,
shader = gl.createShader(shaderType);
// Load the shader source
gl.shaderSource(shader, shaderSource);
// Compile the shader
gl.compileShader(shader);
// Check the compile status
compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
// If something went wrong:
if (!compiled) {
if (error) {
error(
'Error compiling shader "' + shader + '":' +
gl.getShaderInfoLog(shader)
);
}
gl.deleteShader(shader);
return null;
}
return shader;
};
/**
* Creates a program, attaches shaders, binds attrib locations, links the
* program and calls useProgram.
*
* @param {Array.<WebGLShader>} shaders The shaders to attach.
* @param {Array.<string>} attribs The attribs names.
* @param {Array.<number>} locations The locations for the attribs.
* @param {function(string): void} error Callback for errors.
* @return {WebGLProgram} The created program.
*/
sigma.utils.loadProgram = function(gl, shaders, attribs, loc, error) {
var i,
linked,
program = gl.createProgram();
for (i = 0; i < shaders.length; ++i)
gl.attachShader(program, shaders[i]);
if (attribs)
for (i = 0; i < attribs.length; ++i)
gl.bindAttribLocation(
program,
locations ? locations[i] : i,
opt_attribs[i]
);
gl.linkProgram(program);
// Check the link status
linked = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!linked) {
if (error)
error('Error in program linking: ' + gl.getProgramInfoLog(program));
gl.deleteProgram(program);
return null;
}
return program;
};
/**
* *********
* MATRICES:
* *********
* The following utils are just here to help generating the transformation
* matrices for the WebGL renderers.
*/
sigma.utils.pkg('sigma.utils.matrices');
/**
* The returns a 3x3 translation matrix.
*
* @param {number} dx The X translation.
* @param {number} dy The Y translation.
* @return {array} Returns the matrix.
*/
sigma.utils.matrices.translation = function(dx, dy) {
return [
1, 0, 0,
0, 1, 0,
dx, dy, 1
];
};
/**
* The returns a 3x3 or 2x2 rotation matrix.
*
* @param {number} angle The rotation angle.
* @param {boolean} m2 If true, the function will return a 2x2 matrix.
* @return {array} Returns the matrix.
*/
sigma.utils.matrices.rotation = function(angle, m2) {
var cos = Math.cos(angle),
sin = Math.sin(angle);
return m2 ? [
cos, -sin,
sin, cos
] : [
cos, -sin, 0,
sin, cos, 0,
0, 0, 1
];
};
/**
* The returns a 3x3 or 2x2 homothetic transformation matrix.
*
* @param {number} ratio The scaling ratio.
* @param {boolean} m2 If true, the function will return a 2x2 matrix.
* @return {array} Returns the matrix.
*/
sigma.utils.matrices.scale = function(ratio, m2) {
return m2 ? [
ratio, 0,
0, ratio
] : [
ratio, 0, 0,
0, ratio, 0,
0, 0, 1
];
};
/**
* The returns a 3x3 or 2x2 homothetic transformation matrix.
*
* @param {array} a The first matrix.
* @param {array} b The second matrix.
* @param {boolean} m2 If true, the function will assume both matrices are
* 2x2.
* @return {array} Returns the matrix.
*/
sigma.utils.matrices.multiply = function(a, b, m2) {
var l = m2 ? 2 : 3,
a00 = a[0 * l + 0],
a01 = a[0 * l + 1],
a02 = a[0 * l + 2],
a10 = a[1 * l + 0],
a11 = a[1 * l + 1],
a12 = a[1 * l + 2],
a20 = a[2 * l + 0],
a21 = a[2 * l + 1],
a22 = a[2 * l + 2],
b00 = b[0 * l + 0],
b01 = b[0 * l + 1],
b02 = b[0 * l + 2],
b10 = b[1 * l + 0],
b11 = b[1 * l + 1],
b12 = b[1 * l + 2],
b20 = b[2 * l + 0],
b21 = b[2 * l + 1],
b22 = b[2 * l + 2];
return m2 ? [
a00 * b00 + a01 * b10,
a00 * b01 + a01 * b11,
a10 * b00 + a11 * b10,
a10 * b01 + a11 * b11
] : [
a00 * b00 + a01 * b10 + a02 * b20,
a00 * b01 + a01 * b11 + a02 * b21,
a00 * b02 + a01 * b12 + a02 * b22,
a10 * b00 + a11 * b10 + a12 * b20,
a10 * b01 + a11 * b11 + a12 * b21,
a10 * b02 + a11 * b12 + a12 * b22,
a20 * b00 + a21 * b10 + a22 * b20,
a20 * b01 + a21 * b11 + a22 * b21,
a20 * b02 + a21 * b12 + a22 * b22
];
};
}).call(this);

77
demo/worker/benchmark.hpp Normal file
View File

@ -0,0 +1,77 @@
#pragma once
#include <vector>
#include <memory>
#include <chrono>
#include <future>
#include "worker.hpp"
template <class W>
class WorkerRunner
{
public:
WorkerRunner(const std::vector<std::string>& queries)
: worker(std::make_unique<W>(queries)) {}
W* operator->() { return worker.get(); }
const W* operator->() const { return worker.get(); }
void operator()(std::chrono::duration<double> duration)
{
std::packaged_task<WorkerResult()> task([this, duration]() {
return this->worker->benchmark(duration);
});
result = std::move(task.get_future());
std::thread(std::move(task)).detach();
}
std::unique_ptr<W> worker;
std::future<WorkerResult> result;
};
struct Result
{
std::chrono::duration<double> elapsed;
std::vector<uint64_t> requests;
};
Result benchmark(const std::string& host, const std::string& port,
int threads, int connections,
std::chrono::duration<double> duration,
const std::vector<std::string>& queries)
{
std::vector<WorkerRunner<CypherWorker>> workers;
for(int i = 0; i < threads; ++i)
workers.emplace_back(queries);
for(int i = 0; i < connections; ++i)
workers[i % threads]->connect(host, port);
for(auto& worker : workers)
worker(duration);
std::vector<WorkerResult> results;
for(auto& worker : workers)
{
worker.result.wait();
results.push_back(worker.result.get());
}
auto start = std::min_element(results.begin(), results.end(),
[](auto a, auto b) { return a.start < b.start; })->start;
auto end = std::max_element(results.begin(), results.end(),
[](auto a, auto b) { return a.end < b.end; })->end;
std::vector<uint64_t> qps(queries.size(), 0);
for(auto& result : results)
for(size_t i = 0; i < result.requests.size(); ++i)
qps[i] += result.requests[i];
return {end - start, qps};
}

59
demo/worker/client.cpp Normal file
View File

@ -0,0 +1,59 @@
#include <iostream>
#include <cstdlib>
#include <vector>
#include "debug/log.hpp"
#include "benchmark.hpp"
void help()
{
std::cout << "error: too few arguments." << std::endl
<< "usage: host port threads connections duration[s]"
<< std::endl;
std::exit(0);
}
int main(int argc, char* argv[])
{
if(argc < 6)
help();
auto host = std::string(argv[1]);
auto port = std::string(argv[2]);
auto threads = std::stoi(argv[3]);
auto connections = std::stoi(argv[4]);
auto duration = std::stod(argv[5]);
std::vector<std::string> queries {
"CREATE (n{id:@}) RETURN n",
"MATCH (n{id:#}),(m{id:#}) CREATE (n)-[r:test]->(m) RETURN r",
"MATCH (n{id:#}) SET n.prop = ^ RETURN n",
"MATCH (n{id:#})-[r]->(m) RETURN count(r)"
};
std::cout << "Running queries on " << connections << " connections "
<< "using " << threads << " threads "
<< "for " << duration << " seconds." << std::endl
<< "..." << std::endl;
auto result = benchmark(host, port, threads, connections,
std::chrono::duration<double>(duration), queries);
auto& reqs = result.requests;
auto elapsed = result.elapsed.count();
auto total = std::accumulate(reqs.begin(), reqs.end(), 0.0,
[](auto acc, auto x) { return acc + x; }
);
std::cout << "Total of " << total << " requests in "
<< elapsed << "s (" << int(total / elapsed) << " req/s)."
<< std::endl;
for(size_t i = 0; i < queries.size(); ++i)
std::cout << queries[i] << " => "
<< int(reqs[i] / elapsed) << " req/s." << std::endl;
return 0;
}

55
demo/worker/cypher.hpp Normal file
View File

@ -0,0 +1,55 @@
#pragma once
#include <string>
#include "memory/literals.hpp"
#include "cypher_replacer.hpp"
using namespace memory::literals;
class Cypher
{
static std::string body_start;
static std::string body_end;
public:
Cypher()
{
request.reserve(64_kB);
}
std::string& operator()(std::string query)
{
request.clear();
replacer(query);
// request begin and headers
request += "POST /db/data/transaction/commit HTTP/1.1\r\n" \
"Host: localhost:7474\r\n" \
"Authorization: Basic bmVvNGo6cGFzcw==\r\n" \
"Accept: application/json; charset=UTF-8\r\n" \
"Content-Type: application/json\r\n" \
"Content-Length: ";
// content length
auto size = body_start.size() + query.size() + body_end.size();
request += std::to_string(size);
// headers end
request += "\r\n\r\n";
// write body
request += body_start;
request += query;
request += body_end;
return request;
}
private:
std::string request;
CypherReplacer replacer;
};
std::string Cypher::body_start = "{\"statements\":[{\"statement\":\"";
std::string Cypher::body_end = "\"}]}";

View File

@ -0,0 +1,39 @@
#pragma once
#include <random>
#include <atomic>
#include "replacer.hpp"
#include "random.hpp"
static std::atomic<uint64_t> counter {0};
static thread_local std::mt19937 mt;
class CypherReplacer
{
std::uniform_int_distribution<uint64_t> random_int;
RandomString random_string;
public:
CypherReplacer()
{
replacer
.replace("#", [&]() {
return std::to_string(random_int(mt) % (counter.load() + 1));
})
.replace("^", [&]() {
return random_string(mt, 15);
})
.replace("@", [&]() {
return std::to_string(counter.fetch_add(1));
});
}
std::string& operator()(std::string& query)
{
return replacer(query);
}
private:
Replacer replacer;
};

32
demo/worker/random.hpp Normal file
View File

@ -0,0 +1,32 @@
#pragma once
#include <string>
#include <random>
class RandomString
{
static constexpr char charset[] =
"0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz";
public:
template <class Rg>
std::string operator()(Rg&& gen, size_t len)
{
auto str = std::string();
str.reserve(len + 2);
str.push_back('\'');
while(str.size() < len)
str.push_back(charset[rnd(std::forward<Rg>(gen))]);
str.push_back('\'');
return str;
}
private:
std::uniform_int_distribution<> rnd {0, sizeof(charset) - 1};
};
constexpr char RandomString::charset[];

40
demo/worker/replacer.hpp Normal file
View File

@ -0,0 +1,40 @@
#pragma once
#include <vector>
#include <string>
#include <functional>
class Replacer
{
struct Rule
{
std::string match;
std::function<std::string()> generator;
};
public:
Replacer() {}
template <class F>
Replacer& replace(const std::string& match, F&& generator)
{
rules.push_back({match, std::forward<F>(generator)});
return *this;
}
std::string& operator()(std::string& str)
{
size_t n;
for(auto& rule : rules)
{
while((n = str.find_first_of(rule.match)) != std::string::npos)
str.replace(n, rule.match.size(), rule.generator());
}
return str;
}
private:
std::vector<Rule> rules;
};

View File

@ -0,0 +1,26 @@
#pragma once
#include "io/network/client.hpp"
template <class Derived, class Stream>
class SimpleClient : public io::Client<Derived, Stream>
{
char buf[65535];
public:
using Buffer = typename io::StreamReader<Derived, Stream>::Buffer;
void on_wait_timeout() {}
void on_error(Stream&)
{
std::abort();
}
Buffer on_alloc(Stream&)
{
return Buffer { buf, sizeof buf };
}
void on_close(Stream&) {}
};

92
demo/worker/worker.hpp Normal file
View File

@ -0,0 +1,92 @@
#pragma once
#include <functional>
#include <algorithm>
#include <iostream>
#include <random>
#include <vector>
#include <thread>
#include <chrono>
#include <atomic>
#include <future>
#include "simple_client.hpp"
#include "io/network/tcp/stream.hpp"
#include "cypher.hpp"
struct WorkerResult
{
std::chrono::high_resolution_clock::time_point start, end;
std::vector<uint64_t> requests;
};
class CypherWorker : public SimpleClient<CypherWorker, io::tcp::Stream>
{
public:
CypherWorker(const std::vector<std::string>& queries)
: queries(queries), requests(queries.size(), 0) {}
io::tcp::Stream& on_connect(io::Socket&& socket)
{
streams.emplace_back(std::make_unique<io::tcp::Stream>(
std::forward<io::Socket>(socket)
));
return *streams.back();
}
void on_read(io::tcp::Stream& stream, Buffer& buf)
{
/* std::cout << "------------------- RESPONSE ------------------" << std::endl; */
/* std::cout << std::string(buf.ptr, buf.len) << std::endl; */
/* std::cout << "-----------------------------------------------" << std::endl; */
/* std::cout << std::endl; */
send(stream.socket);
}
void send(io::Socket& socket)
{
auto idx = random_int(mt) % queries.size();
// increase the number of requests
requests[idx]++;
// cypherize and send the request
//socket.write(cypher(queries[idx]));
auto req = cypher(queries[idx]);
/* std::cout << "-------------------- REQUEST ------------------" << std::endl; */
/* std::cout << req << std::endl; */
socket.write(req);
}
WorkerResult benchmark(std::chrono::duration<double> duration)
{
using clock = std::chrono::high_resolution_clock;
clock::time_point end, start = clock::now();
for(auto& stream : streams)
send(stream->socket);
while(true)
{
this->wait_and_process_events();
if((end = clock::now()) - start > duration)
break;
}
return {start, end, requests};
}
private:
std::uniform_int_distribution<> random_int;
Cypher cypher;
std::vector<std::unique_ptr<io::tcp::Stream>> streams;
std::vector<std::string> queries;
std::vector<uint64_t> requests;
};

293
examples/client.cpp Normal file
View File

@ -0,0 +1,293 @@
#include <functional>
#include <algorithm>
#include <iostream>
#include <random>
#include <vector>
#include <thread>
#include <chrono>
#include <atomic>
#include <future>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include "debug/log.hpp"
#include "io/network/epoll.hpp"
#include "io/network/socket.hpp"
#include "io/network/tcp/stream.hpp"
#include "io/network/stream_reader.hpp"
#include "memory/literals.hpp"
using namespace memory::literals;
class RandomString
{
static constexpr char charset[] =
"0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz";
public:
template <class Rg>
std::string operator()(Rg&& gen, size_t len)
{
auto str = std::string();
str.reserve(len + 2);
str.push_back('\'');
while(str.size() < len)
str.push_back(charset[rnd(std::forward<Rg>(gen))]);
str.push_back('\'');
return str;
}
private:
std::uniform_int_distribution<> rnd {0, sizeof(charset) - 1};
};
constexpr char RandomString::charset[];
static std::mt19937 mt {std::random_device{}()};
class CypherPost
{
static std::string templ;
public:
CypherPost()
{
request.reserve(64_kB);
}
void set(const std::string& query)
{
request.clear();
request += "POST /db/data/transaction/commit HTTP/1.1\r\n" \
"Host: localhost:7474\r\n" \
"Authorization: Basic bmVvNGo6cGFzcw==\r\n" \
"Accept: application/json; charset=UTF-8\r\n" \
"Content-Type: application/json\r\n" \
"Content-Length: ";
request += std::to_string(query.size() + templ.size() + 4);
request += "\r\n\r\n";
request += templ;
request += query;
request += "\"}]}";
}
operator const std::string&() const { return request; }
private:
std::string request;
};
std::string CypherPost::templ = "{\"statements\":[{\"statement\":\"";
struct Result
{
std::chrono::high_resolution_clock::time_point start, end;
uint64_t requests;
};
class Worker : public io::StreamReader<Worker, io::tcp::Stream>
{
char buf[65535];
CypherPost post;
std::uniform_int_distribution<> random_int;
RandomString random_string;
Replacer replacer;
public:
Worker()
{
replacer.replace("#", [&]() { return std::to_string(random_int(mt)); })
.replace("^", [&]() { return random_string(mt, 15); });
}
io::tcp::Stream& on_connect(io::Socket&&)
{
// DUMMY, refactor StreamReader to be more generic
return *streams.back();
}
bool connect(const char* name, const char* port)
{
auto socket = io::Socket::connect(name, port);
if(!socket.is_open())
return false;
socket.set_non_blocking();
streams.push_back(std::make_unique<io::tcp::Stream>(std::move(socket)));
auto& stream = streams.back();
stream->event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
this->add(*stream);
return true;
}
void on_error(io::tcp::Stream& conn)
{
LOG_DEBUG("error on socket " << conn.id());
(void)conn;
LOG_DEBUG((errno == EBADF));
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::abort();
}
void on_wait_timeout() {}
Buffer on_alloc(io::tcp::Stream&)
{
return Buffer { buf, sizeof buf };
}
void on_read(io::tcp::Stream& stream, Buffer& buf)
{
/* std::cout << "RESPONSE" << std::endl; */
/* std::cout << std::string(buf.ptr, buf.len) << std::endl; */
requests++;
LOG_DEBUG("on_read");
sendreq(stream.socket);
}
void on_close(io::tcp::Stream&) {}
void sendreq(io::Socket& socket)
{
/* auto query = std::string("CREATE (n:Person {id: #, name: ^}) RETURN n"); */
auto query = std::string("MATCH (n:Person {id: #}) RETURN n");
post.set(replacer(query));
/* std::cout << "REQUEST" << std::endl; */
/* std::cout << static_cast<const std::string&>(post) << std::endl; */
/* std::cout << "SIZE = " << static_cast<const std::string&>(post).size() << std::endl; */
auto n = socket.write(static_cast<const std::string&>(post));
/* std::cout << "Written N = " << n << " bytes." << std::endl; */
LOG_DEBUG("sent.");
}
Result run_benchmark(std::chrono::duration<double> duration)
{
LOG_DEBUG("run_benchmark");
using clock = std::chrono::high_resolution_clock;
clock::time_point end, start = clock::now();
for(auto& stream : streams)
sendreq(stream->socket);
LOG_DEBUG("sent req to all streams");
while(true)
{
LOG_DEBUG("WAIT AND PROCESS");
this->wait_and_process_events();
if((end = clock::now()) - start > duration)
break;
}
return {start, end, requests};
}
private:
uint64_t requests {0};
std::vector<std::unique_ptr<io::tcp::Stream>> streams;
};
class WorkerRunner
{
public:
WorkerRunner() : worker(std::make_unique<Worker>()) {}
Worker* operator->() { return worker.get(); }
const Worker* operator->() const { return worker.get(); }
void operator()(std::chrono::duration<double> duration)
{
std::packaged_task<Result()> task([this, duration]() {
return this->worker->run_benchmark(duration);
});
result = std::move(task.get_future());
std::thread(std::move(task)).detach();
}
std::unique_ptr<Worker> worker;
std::future<Result> result;
};
std::atomic<bool> alive {true};
int main(int argc, const char* argv[])
{
using clock = std::chrono::high_resolution_clock;
using namespace std::chrono;
if(argc < 4)
std::abort();
auto threads = std::stoi(argv[1]);
auto connections = std::stoi(argv[2]);
auto duration = std::stoi(argv[3]);
std::vector<WorkerRunner> workers;
for(int i = 0; i < threads; ++i)
workers.emplace_back();
for(int i = 0; i < connections; ++i)
workers[i % threads]->connect("localhost", "7474");
std::vector<Result> results;
std::cout << "Running queries on " << connections << " connections "
<< "using " << threads << " threads "
<< "for " << duration << " seconds." << std::endl
<< "..." << std::endl;
for(auto& worker : workers)
worker(std::chrono::seconds(duration));
for(auto& worker : workers)
{
worker.result.wait();
results.push_back(worker.result.get());
}
auto start = std::min_element(results.begin(), results.end(),
[](auto a, auto b) { return a.start < b.start; })->start;
auto end = std::max_element(results.begin(), results.end(),
[](auto a, auto b) { return a.end < b.end; })->end;
auto requests = std::accumulate(results.begin() + 1, results.end(),
results[0].requests, [](auto acc, auto r) { return acc + r.requests; });
auto elapsed = (end - start).count() / 1.0e9;
std::cout << "Total of " << requests << " requests in "
<< elapsed << "s." << std::endl
<< "Requests/sec: " << int(requests / elapsed)
<< "." << std::endl;
return 0;
}

59
examples/index.cpp Normal file
View File

@ -0,0 +1,59 @@
#include <iostream>
#include "transactions/engine.hpp"
#include "mvcc/version_list.hpp"
#include "storage/vertex.hpp"
#include "storage/indexes/property_index.hpp"
using std::cout;
using std::endl;
using Record = mvcc::VersionList<Vertex>;
tx::Engine engine;
Index<Vertex>* index = new PropertyIndex<Vertex>();
Record* create(int id, tx::Transaction& t)
{
auto v = new record_t(id);
auto a = v->access(t);
a.insert();
return v;
}
auto insert(int id, tx::Transaction& t)
{
auto r = create(id, t);
return index.insert(&r->id, r, t);
}
int main(void)
{
/* v1->data.props.set<String>("name", "buda"); */
/* v1->data.props.set<Int32>("age", 23); */
auto& t1 = engine.begin();
insert(0, t1);
insert(1, t1);
insert(2, t1);
insert(3, t1);
insert(6, t1);
insert(7, t1);
t1.commit();
auto& t2 = engine.begin();
insert(4, t2);
insert(5, t2);
insert(8, t2);
t2.commit();
auto& t3 = engine.begin();
auto cursor = index.scan(8, t3);
for(; not cursor.end(); cursor++)
cout << cursor->id() << endl;
return 0;
}

View File

@ -36,6 +36,12 @@ int main(void)
props.set<Float>("pi", z);
cout << props.at("awesome") << endl;
cout << props.at("lame") << endl;
cout << props.at("age") << endl;
cout << props.at("pi") << endl;
cout << props.at("lol") << endl;
StringBuffer buffer;
JsonWriter<StringBuffer> writer(buffer);

27
examples/strcmp.cpp Normal file
View File

@ -0,0 +1,27 @@
#include <iostream>
#include <fstream>
#include <cstring>
std::string load_file(const std::string& fname)
{
std::ifstream in(fname);
return std::string((std::istreambuf_iterator<char>(in)),
std::istreambuf_iterator<char>());
}
int main(int argc, const char* argv[])
{
if(argc < 3)
return -1;
auto a = load_file(argv[1]);
auto b = load_file(argv[2]);
bool result = true;
for(size_t i = 0; i < a.size(); ++i)
result &= strcmp(a.c_str() + i, b.c_str() + i) == 0;
std::cout << result << std::endl;
return 0;
}

24
examples/test.cpp Normal file
View File

@ -0,0 +1,24 @@
#include <iostream>
class A
{
public:
class B;
};
class A::B
{
public:
};
int main(void)
{
A a;
A::B b;
return 0;
}

View File

@ -1,23 +1,16 @@
#pragma once
#include "io/network/tcp_stream.hpp"
#include "io/network/tcp/stream.hpp"
#include "memory/literals.hpp"
namespace htpp
namespace http
{
using memory::literals::operator "" _kB;
using namespace memory::literals;
class Connection
template <class Req, class Res>
class Connection : public io::tcp::Stream
{
Connection(io::Socket&& socket) : stream(std::move(socket))
{
stream.data = this;
}
void close()
{
delete reinterpret_cast<Connection*>(stream.data);
}
public:
struct Buffers
{
char headers[8_kB];
@ -26,11 +19,14 @@ class Connection
static constexpr size_t size = sizeof headers + sizeof body;
};
Connection(io::Socket&& socket) : io::tcp::Stream(std::move(socket)),
response(this->socket) {}
// tcp stream reads into these buffers
Buffers buffers;
// tcp stream this connection reads from
io::TcpStream stream;
Req request;
Res response;
};
}

37
http/headers.hpp Normal file
View File

@ -0,0 +1,37 @@
#pragma once
#include <array>
#include "utils/string/weak_string.hpp"
namespace http
{
class Headers
{
static constexpr WeakString empty = WeakString();
public:
struct Header
{
WeakString key;
WeakString value;
};
std::pair<bool, WeakString&> operator[](const std::string& key)
{
auto ww = WeakString();
for(auto& header : headers)
if(key == header.key)
return {true, header.value};
return {false, ww};
}
private:
std::array<Header, 64> headers;
};
}

View File

@ -17,7 +17,7 @@ int main(void)
io::TcpServer<http::Worker> server;
server.bind("0.0.0.0", "7474").listen(8, 128, []() {
std::cout << "response!" << std::endl;
std::cout << "response!" << std::endl;
});
return 0;

13
http/request.hpp Normal file
View File

@ -0,0 +1,13 @@
#pragma once
namespace http
{
class Request
{
public:
Request() = default;
};
}

61
http/response.hpp Normal file
View File

@ -0,0 +1,61 @@
#pragma once
#include <array>
#include "io/network/socket.hpp"
#include "memory/literals.hpp"
namespace http
{
using namespace memory::literals;
class Response
{
template <size_t capacity = 64_kB>
struct Buffer
{
public:
Buffer() = default;
size_t write(const char* data, size_t n)
{
::memcpy(buf + len, data, std::max(n, capacity - len));
}
char buf[capacity];
size_t len {0};
};
public:
Response(io::Socket& socket) : socket(socket) {}
Response& write(const char* data, size_t len)
{
/* while(true) */
/* { */
/* auto bytes_written = buffer.write(data, len); */
/* if(len - bytes_written == 0) */
/* break; */
/* len -= bytes_written, data += bytes_written; */
/* char* ptr = buffer.buf; */
/* socket.write(p */
/* } */
return *this;
}
private:
io::Socket& socket;
Buffer<64_kB> buffer;
/* void flush_socket() */
};
}

View File

@ -1,64 +1,61 @@
#pragma once
#include "io/network/tcp_reader.hpp"
#include "io/network/server.hpp"
#include "debug/log.hpp"
#include "connection.hpp"
namespace http
{
const char* body = "Now that the internet can be accessed on any mobile device, whether a laptop, desktop, tablets, smartphone, websites are designed in responsive web version. It is the ability to change the page and font size according to the screen size of the user. desktop, tablets, smartphone, websites are designed in responsive web version. It is the ability to change the page and font size according to the screen size of the user. Thus a website is accessible anytime on any instrument. CSS3 frameworks were widely accepted in 2014 and is growing in 2015. It reduces time and money by helping not creating different sites for different users";
/* const char* body = "Now that the internet can be accessed on any mobile device, whether a laptop, desktop, tablets, smartphone, websites are designed in responsive web version. It is the ability to change the page and font size according to the screen size of the user. desktop, tablets, smartphone, websites are designed in responsive web version. It is the ability to change the page and font size according to the screen size of the user. Thus a website is accessible anytime on any instrument. CSS3 frameworks were widely accepted in 2014 and is growing in 2015. It reduces time and money by helping not creating different sites for different users"; */
std::string response = "HTTP/1.1 200 OK\r\nContent-Length:"
+ std::to_string(strlen(body)) + "\r\n\r\n" + body;
/* const char* body = ""; */
class Worker : public io::TcpReader<Worker>
std::string response = "HTTP/1.1 200 OK\r\nContent-Length:0\r\n\r\n";
template <class Req, class Res>
class Parser : public io::Server<Parser<Req, Res>, Connection<Req, Res>>
{
using Connection = Connection<Req, Res>;
using Buffer = typename io::StreamReader<Parser<Req, Res>, Connection>::Buffer;
public:
char buf[65536];
Worker() = default;
Parser() = default;
io::TcpStream& on_connect(io::Socket&& socket)
Connection& on_connect(io::Socket&& socket)
{
auto stream = new io::TcpStream(std::move(socket));
auto stream = new Connection(std::move(socket));
LOG_DEBUG("on_connect socket " << stream->id());
return *stream;
}
void on_error(io::TcpStream& stream)
void on_error(Connection& conn)
{
LOG_DEBUG("on_error: " << stream.id());
LOG_DEBUG("on_error: " << conn.id());
}
void on_wait_timeout()
{
LOG_DEBUG("Worker on_wait_timeout");
}
void on_wait_timeout() {}
Buffer on_alloc(size_t suggested_size)
Buffer on_alloc(Connection& conn)
{
LOG_DEBUG("Allocating buffer");
LOG_DEBUG("on_alloc socket " << conn.id());
return Buffer { buf, sizeof buf };
}
void on_read(io::TcpStream& stream, Buffer& buf)
void on_read(Connection& conn, Buffer& buf)
{
LOG_DEBUG("on_read (socket: " << stream.id() << "): '"
<< std::string(buf.ptr, buf.len) << "'");
auto& socket = stream.socket;
auto n = write(socket, response.c_str(), response.size());
LOG_DEBUG("Responded with " << n << " characters");
LOG_DEBUG("on_read socket " << conn.id());
auto& socket = conn.socket;
socket.write(response.c_str(), response.size());
}
void on_close(io::TcpStream& stream)
void on_close(Connection& conn)
{
LOG_DEBUG("on_close: " << stream.id());
stream.close();
LOG_DEBUG("on_close socket " << conn.id());
conn.close();
}
};

35
io/network/client.hpp Normal file
View File

@ -0,0 +1,35 @@
#pragma once
#include "stream_reader.hpp"
namespace io
{
template <class Derived, class Stream>
class Client : public StreamReader<Derived, Stream>
{
public:
bool connect(const std::string& host, const std::string& port)
{
return connect(host.c_str(), port.c_str());
}
bool connect(const char* host, const char* port)
{
auto socket = io::Socket::connect(host, port);
if(!socket.is_open())
return false;
socket.set_non_blocking();
auto& stream = this->derived().on_connect(std::move(socket));
stream.event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
this->add(stream);
return true;
}
};
}

View File

@ -2,22 +2,21 @@
#include "socket.hpp"
#include "epoll.hpp"
#include "tcp_stream.hpp"
#include "utils/crtp.hpp"
namespace io
{
template <class Derived, size_t max_events = 64, int wait_timeout = -1>
class TcpListener : public Crtp<Derived>
template <class Derived, class Stream,
size_t max_events = 64, int wait_timeout = -1>
class EventListener : public Crtp<Derived>
{
public:
using Crtp<Derived>::derived;
TcpListener(uint32_t flags = 0) : listener(flags) {}
EventListener(uint32_t flags = 0) : listener(flags) {}
void add(TcpStream& stream)
void add(Stream& stream)
{
// add the stream to the event listener
listener.add(stream.socket, &stream.event);
@ -30,15 +29,18 @@ public:
// wait_timeout milliseconds. if wait_timeout is achieved, returns 0
auto n = listener.wait(events, max_events, wait_timeout);
LOG_DEBUG("received " << n << " events");
// go through all events and process them in order
for(int i = 0; i < n; ++i)
{
auto& event = events[i];
auto& stream = *reinterpret_cast<TcpStream*>(event.data.ptr);
auto& stream = *reinterpret_cast<Stream*>(event.data.ptr);
// a tcp stream was closed
// a stream was closed
if(UNLIKELY(event.events & EPOLLRDHUP))
{
LOG_DEBUG("EPOLLRDHUP event recieved on socket " << stream.id());
this->derived().on_close(stream);
continue;
}
@ -47,10 +49,16 @@ public:
if(UNLIKELY(!(event.events & EPOLLIN) ||
event.events & (EPOLLHUP | EPOLLERR)))
{
LOG_DEBUG(">> EPOLL ERR");
LOG_DEBUG("EPOLLIN" << (event.events & EPOLLIN));
LOG_DEBUG("EPOLLHUP" << (event.events & EPOLLHUP));
LOG_DEBUG("EPOLLERR" << (event.events & EPOLLERR));
this->derived().on_error(stream);
continue;
}
LOG_DEBUG("signalling that data exists on socket " << stream.id());
// we have some data waiting to be read
this->derived().on_data(stream);
}

37
io/network/server.hpp Normal file
View File

@ -0,0 +1,37 @@
#pragma once
#include "stream_reader.hpp"
namespace io
{
template <class Derived, class Stream>
class Server : public StreamReader<Derived, Stream>
{
public:
bool accept(Socket& socket)
{
// accept a connection from a socket
auto s = socket.accept(nullptr, nullptr);
LOG_DEBUG("socket " << s.id() << " accepted");
if(!s.is_open())
return false;
// make the recieved socket non blocking
s.set_non_blocking();
auto& stream = this->derived().on_connect(std::move(s));
// we want to listen to an incoming event which is edge triggered and
// we also want to listen on the hangup event
stream.event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
// add the connection to the event listener
this->add(stream);
return true;
}
};
}

View File

@ -56,6 +56,34 @@ public:
return socket != -1;
}
static Socket connect(const std::string& addr, const std::string& port)
{
return connect(addr.c_str(), port.c_str());
}
static Socket connect(const char* addr, const char* port)
{
auto info = AddrInfo::get(addr, port);
for(struct addrinfo* it = info; it != nullptr; it = it->ai_next)
{
auto s = Socket(it->ai_family, it->ai_socktype, it->ai_protocol);
if(!s.is_open())
continue;
if(::connect(s, it->ai_addr, it->ai_addrlen) == 0)
return std::move(s);
}
throw NetworkError("Unable to connect to socket");
}
static Socket bind(const std::string& addr, const std::string& port)
{
return bind(addr.c_str(), port.c_str());
}
static Socket bind(const char* addr, const char* port)
{
auto info = AddrInfo::get(addr, port);
@ -113,6 +141,21 @@ public:
return socket;
}
size_t write(const std::string& str)
{
return ::write(socket, str.c_str(), str.size());
}
size_t write(const char* data, size_t len)
{
return ::write(socket, data, len);
}
size_t read(char* buffer, size_t len)
{
return ::read(socket, buffer, len);
}
private:
int socket;
};

View File

@ -0,0 +1,60 @@
#pragma once
#include "event_listener.hpp"
#include "memory/literals.hpp"
namespace io
{
using namespace memory::literals;
template <class Derived, class Stream>
class StreamReader : public EventListener<Derived, Stream>
{
public:
struct Buffer
{
char* ptr;
size_t len;
};
StreamReader(uint32_t flags = 0) : EventListener<Derived, Stream>(flags) {}
void on_data(Stream& stream)
{
while(true)
{
// allocate the buffer to fill the data
auto buf = this->derived().on_alloc(stream);
// read from the buffer at most buf.len bytes
buf.len = stream.socket.read(buf.ptr, buf.len);
// check for read errors
if(buf.len == -1)
{
// this means we have read all available data
if(LIKELY(errno == EAGAIN))
{
LOG_DEBUG("EAGAIN read all data on socket " << stream.id());
break;
}
// some other error occurred, check errno
this->derived().on_error(stream);
}
// end of file, the client has closed the connection
if(UNLIKELY(buf.len == 0))
{
LOG_DEBUG("EOF stream closed on socket " << stream.id());
stream.close();
break;
}
LOG_DEBUG("data on socket " << stream.id());
this->derived().on_read(stream, buf);
}
}
};
}

39
io/network/tcp/stream.hpp Normal file
View File

@ -0,0 +1,39 @@
#pragma once
#include "io/network/epoll.hpp"
#include "io/network/socket.hpp"
namespace io
{
namespace tcp
{
class Stream
{
public:
Stream(Socket&& socket) : socket(std::move(socket))
{
// save this to epoll event data baton to access later
event.data.ptr = this;
}
Stream(Stream&& stream)
{
socket = std::move(stream.socket);
event = stream.event;
event.data.ptr = this;
}
void close()
{
delete reinterpret_cast<Stream*>(event.data.ptr);
}
int id() const { return socket.id(); }
Socket socket;
Epoll::Event event;
};
}
}

View File

@ -1,91 +0,0 @@
#pragma once
#include "tcp_listener.hpp"
#include "tcp_stream.hpp"
namespace io
{
template <class Derived>
class TcpReader : public TcpListener<TcpReader<Derived>>, public Crtp<Derived>
{
using listener_t = TcpListener<TcpReader<Derived>>;
using Crtp<Derived>::derived;
public:
TcpReader() : listener_t(0) {}
struct Buffer
{
char* ptr;
size_t len;
};
bool accept(Socket& socket)
{
// accept a connection from a socket
auto s = socket.accept(nullptr, nullptr);
if(!s.is_open())
return false;
// make the recieved socket non blocking
s.set_non_blocking();
auto& stream = derived().on_connect(std::move(s));
// we want to listen to an incoming event whish is edge triggered and
// we also want to listen on the hangup event
stream.event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
// add the connection to the event listener
this->add(stream);
return true;
}
void on_close(TcpStream& stream)
{
derived().on_close(stream);
}
void on_error(TcpStream& stream)
{
derived().on_error(stream);
}
void on_wait_timeout()
{
derived().on_wait_timeout();
}
void on_data(TcpStream& stream)
{
constexpr size_t suggested_size = 64_kB;
while(true)
{
Buffer buf = derived().on_alloc(suggested_size);
buf.len = read(stream.socket, buf.ptr, buf.len);
if(buf.len == -1)
{
if(UNLIKELY(errno != EAGAIN))
derived().on_error(stream);
break;
}
if(UNLIKELY(buf.len == 0))
{
stream.close();
break;
}
derived().on_read(stream, buf);
}
}
};
}

View File

@ -1,33 +0,0 @@
#pragma once
#include "epoll.hpp"
#include "socket.hpp"
#include "memory/literals.hpp"
namespace io
{
class TcpStream
{
public:
TcpStream(Socket&& socket) : socket(std::move(socket))
{
event.data.ptr = this;
}
void close()
{
delete reinterpret_cast<TcpStream*>(event.data.ptr);
}
int id() const { return socket.id(); }
Socket socket;
Epoll::Event event;
// custom data we can pass on
void* data;
};
}

Binary file not shown.

View File

@ -3,15 +3,20 @@
#include <thread>
#include <signal.h>
std::hash<std::thread::id> hash;
#include "debug/log.hpp"
#include "http/request.hpp"
#include "http/response.hpp"
#include "socket.hpp"
#include "http/worker.hpp"
std::array<http::Worker, 16> workers;
std::array<std::thread, 16> threads;
std::hash<std::thread::id> hash;
constexpr unsigned K = 128;
std::array<http::Parser<http::Request, http::Response>, K> workers;
std::array<std::thread, K> threads;
std::atomic<bool> alive { true };
@ -24,25 +29,24 @@ void sigint_handler(int)
{
exiting();
std::abort();
std::exit(0);
}
#define MAXEVENTS 64
int main(void)
{
std::atexit(exiting);
//std::atexit(exiting);
signal(SIGINT, sigint_handler);
for(size_t i = 0; i < workers.size(); ++i)
{
auto& w = workers[i];
threads[i] = std::thread([&w]() {
threads[i] = std::thread([i, &w]() {
while(alive)
{
LOG_DEBUG("Worker " << hash(std::this_thread::get_id())
<< " waiting... ");
LOG_DEBUG("waiting for events on thread " << i);
w.wait_and_process_events();
}
});
@ -93,8 +97,11 @@ int main(void)
{
int n, i;
LOG_DEBUG("acceptor waiting for events");
n = epoll_wait (efd, events, MAXEVENTS, -1);
LOG_DEBUG("acceptor recieved " << n << " connection requests");
for (i = 0; i < n; i++)
{
if ((events[i].events & EPOLLERR) ||
@ -109,123 +116,20 @@ int main(void)
}
else if (socket == events[i].data.fd)
{
{
/* We have a notification on the listening socket, which
means one or more incoming connections. */
while (true)
{
LOG_DEBUG("Trying to accept... ");
LOG_DEBUG("trying to accept connection on thread " << idx);
if(!workers[idx].accept(socket))
{
LOG_DEBUG("Did not accept!");
break;
}
LOG_DEBUG("Accepted a new connection on thread " << idx);
idx = (idx + 1) % workers.size();
LOG_DEBUG("Accepted a new connection!");
break;
}
/* struct sockaddr in_addr; */
/* socklen_t in_len; */
/* int infd; */
/* char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV]; */
/* in_len = sizeof in_addr; */
/* infd = accept (socket, &in_addr, &in_len); */
/* if (infd == -1) */
/* { */
/* if ((errno == EAGAIN) || */
/* (errno == EWOULDBLOCK)) */
/* { */
/* /1* We have processed all incoming */
/* connections. *1/ */
/* break; */
/* } */
/* else */
/* { */
/* perror ("accept"); */
/* break; */
/* } */
/* } */
/* s = getnameinfo (&in_addr, in_len, */
/* hbuf, sizeof hbuf, */
/* sbuf, sizeof sbuf, */
/* NI_NUMERICHOST | NI_NUMERICSERV); */
/* if (s == 0) */
/* { */
/* printf("Accepted connection on descriptor %d " */
/* "(host=%s, port=%s)\n", infd, hbuf, sbuf); */
/* } */
/* /1* Make the incoming socket non-blocking and add it to the */
/* list of fds to monitor. *1/ */
/* s = make_socket_non_blocking (infd); */
/* if (s == -1) */
/* abort (); */
/* event.data.fd = infd; */
/* event.events = EPOLLIN | EPOLLET; */
/* s = epoll_ctl (efd, EPOLL_CTL_ADD, infd, &event); */
/* if (s == -1) */
/* { */
/* perror ("epoll_ctl"); */
/* abort (); */
/* } */
}
/* else */
/* { */
/* /1* We have data on the fd waiting to be read. Read and */
/* display it. We must read whatever data is available */
/* completely, as we are running in edge-triggered mode */
/* and won't get a notification again for the same */
/* data. *1/ */
/* int done = 0; */
/* while (1) */
/* { */
/* ssize_t count; */
/* char buf[512]; */
/* count = read (events[i].data.fd, buf, sizeof buf); */
/* if (count == -1) */
/* { */
/* /1* If errno == EAGAIN, that means we have read all */
/* data. So go back to the main loop. *1/ */
/* if (errno != EAGAIN) */
/* { */
/* perror ("read"); */
/* done = 1; */
/* } */
/* break; */
/* } */
/* else if (count == 0) */
/* { */
/* /1* End of file. The remote has closed the */
/* connection. *1/ */
/* done = 1; */
/* break; */
/* } */
/* /1* Write the buffer to standard output *1/ */
/* s = write (1, buf, count); */
/* if (s == -1) */
/* { */
/* perror ("write"); */
/* abort (); */
/* } */
/* } */
/* if (done) */
/* { */
/* printf ("Closed connection on descriptor %d\n", */
/* events[i].data.fd); */
/* /1* Closing the descriptor will make epoll remove it */
/* from the set of descriptors which are monitored. *1/ */
/* close (events[i].data.fd); */
/* } */
/* } */
}
}

View File

@ -11,7 +11,7 @@ namespace io
class Worker : public Listener<Worker>
{
char buf[512];
char buf[64_kB];
public:
using Listener::Listener;

View File

@ -31,7 +31,7 @@ public:
uv::TcpStream client;
parser_t parser;
Req request;
Res response;

View File

@ -42,7 +42,7 @@ void Response<Req, Res>::send(const std::string& body)
buffer << "\r\n" << body;
uv_write(write_req, connection.client, buffer, buffer.count(),
uv_write(write_req, connection.client, buffer, buffer.count(),
[](uv_write_t* write_req, int) {
connection_t& conn = *reinterpret_cast<connection_t*>(write_req->data);

View File

@ -7,38 +7,30 @@
namespace
{
using std::string;
using std::vector;
using vector_str = vector<string>;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-function"
decltype(auto) all_arguments(int argc, char *argv[])
auto all_arguments(int argc, char *argv[])
{
vector_str args(argv + 1, argv + argc);
return args;
return std::vector<std::string>(argv + 1, argv + argc);
}
bool contain_argument(const vector_str& all, const string& flag)
bool contains_argument(const std::vector<std::string>& all,
const std::string& flag)
{
// TODO: optimize this implementation
auto it = std::find(all.begin(), all.end(), flag);
if (it == all.end())
return false;
return true;
return std::find(all.begin(), all.end(), flag) != all.end();
}
decltype(auto) get_argument(const vector_str& all,
const std::string& flag,
const std::string& default_value)
auto get_argument(const std::vector<std::string>& all,
const std::string& flag,
const std::string& default_value)
{
// TODO: optimize this implementation
auto it = std::find(all.begin(), all.end(), flag);
if (it == all.end())
if(it == all.end())
return default_value;
auto pos = std::distance(all.begin(), it);
return all[pos + 1];
return all[std::distance(all.begin(), it) + 1];
}
#pragma clang diagnostic pop

37
utils/string/streq.hpp Normal file
View File

@ -0,0 +1,37 @@
#pragma once
#include <x86intrin.h>
namespace sse42
{
constexpr int strcmp_mode = _SIDD_CMP_EQUAL_EACH
| _SIDD_NEGATIVE_POLARITY;
constexpr unsigned CF = 0x1;
constexpr unsigned ZF = 0x40;
bool streq(const char* lhs, const char* rhs)
{
int idx, eflags;
while(true)
{
auto lhs_mm = _mm_loadu_si128((__m128i*)lhs);
auto rhs_mm = _mm_loadu_si128((__m128i*)rhs);
idx = _mm_cmpistri(lhs_mm, rhs_mm, strcmp_mode);
eflags = __readeflags();
if(idx != 0x10)
return false;
if((eflags & (ZF | CF)) != 0)
return true;
lhs += 16;
rhs += 16;
}
}
}

View File

@ -0,0 +1,86 @@
#pragma once
#include <cstring>
#include <cassert>
#include <string>
#include "utils/total_ordering.hpp"
#include "utils/total_ordering_with.hpp"
class WeakString
{
public:
constexpr WeakString() : str(nullptr), len(0) {}
WeakString(const std::string& str) : str(str.c_str()), len(str.size()) {}
WeakString(const char* str) : str(str), len(strlen(str)) {}
constexpr WeakString(const char* str, size_t len) : str(str), len(len) {}
const char& operator[](size_t idx) const
{
assert(idx < len);
return str[idx];
}
const char& front() const
{
assert(len > 0);
return str[0];
}
const char& back() const
{
assert(len > 0);
return str[len - 1];
}
const char* data() const
{
return str;
}
bool empty() const
{
return len == 0;
}
size_t size() const
{
return len;
}
size_t length() const
{
return size();
}
std::string to_string() const
{
return std::string(str, len);
}
friend bool operator==(const WeakString& lhs, const WeakString rhs)
{
// oh dear god, make this better with custom iterators
if(lhs.size() != rhs.size())
return false;
for(size_t i = 0; i < lhs.size(); ++i)
if(lhs[i] != rhs[i])
return false;
return true;
}
friend bool operator!=(const WeakString& lhs, const WeakString& rhs)
{
return !(lhs == rhs);
}
private:
const char* str;
size_t len;
};