diff --git a/HISTORY.md b/HISTORY.md
index 4372eeff..b4e4e10d 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -18,6 +18,7 @@ http://visjs.org
- Changed group behaviour, groups now extend the options, not replace. This allows partial defines of color.
- Fixed bug where box shaped nodes did not use hover color.
- Fixed Locales docs.
+- When hovering over a node that does not have a title, the title of one of the connected edges that HAS a title is no longer shown.
### Graph2d
diff --git a/dist/vis.js b/dist/vis.js
index 2eb73df0..937e0cb3 100644
--- a/dist/vis.js
+++ b/dist/vis.js
@@ -106,7 +106,7 @@ return /******/ (function(modules) { // webpackBootstrap
exports.Graph2d = __webpack_require__(42);
exports.timeline = {
DateUtil: __webpack_require__(24),
- DataStep: __webpack_require__(44),
+ DataStep: __webpack_require__(45),
Range: __webpack_require__(21),
stack: __webpack_require__(28),
TimeStep: __webpack_require__(38),
@@ -123,7 +123,7 @@ return /******/ (function(modules) { // webpackBootstrap
Component: __webpack_require__(23),
CurrentTime: __webpack_require__(39),
CustomTime: __webpack_require__(41),
- DataAxis: __webpack_require__(45),
+ DataAxis: __webpack_require__(44),
GraphGroup: __webpack_require__(46),
Group: __webpack_require__(27),
BackgroundGroup: __webpack_require__(31),
@@ -137,13 +137,13 @@ return /******/ (function(modules) { // webpackBootstrap
// Network
exports.Network = __webpack_require__(51);
exports.network = {
- Edge: __webpack_require__(57),
+ Edge: __webpack_require__(52),
Groups: __webpack_require__(54),
Images: __webpack_require__(55),
- Node: __webpack_require__(56),
- Popup: __webpack_require__(58),
- dotparser: __webpack_require__(52),
- gephiParser: __webpack_require__(53)
+ Node: __webpack_require__(53),
+ Popup: __webpack_require__(56),
+ dotparser: __webpack_require__(57),
+ gephiParser: __webpack_require__(58)
};
// Deprecated since v3.0.0
@@ -19640,7 +19640,7 @@ return /******/ (function(modules) { // webpackBootstrap
var DataSet = __webpack_require__(7);
var DataView = __webpack_require__(9);
var Component = __webpack_require__(23);
- var DataAxis = __webpack_require__(45);
+ var DataAxis = __webpack_require__(44);
var GraphGroup = __webpack_require__(46);
var Legend = __webpack_require__(50);
var BarGraphFunctions = __webpack_require__(49);
@@ -20636,582 +20636,301 @@ return /******/ (function(modules) { // webpackBootstrap
/* 44 */
/***/ function(module, exports, __webpack_require__) {
+ var util = __webpack_require__(1);
+ var DOMutil = __webpack_require__(6);
+ var Component = __webpack_require__(23);
+ var DataStep = __webpack_require__(45);
+
/**
- * @constructor DataStep
- * The class DataStep is an iterator for data for the lineGraph. You provide a start data point and an
- * end data point. The class itself determines the best scale (step size) based on the
- * provided start Date, end Date, and minimumStep.
- *
- * If minimumStep is provided, the step size is chosen as close as possible
- * to the minimumStep but larger than minimumStep. If minimumStep is not
- * provided, the scale is set to 1 DAY.
- * The minimumStep should correspond with the onscreen size of about 6 characters
- *
- * Alternatively, you can set a scale by hand.
- * After creation, you can initialize the class by executing first(). Then you
- * can iterate from the start date to the end date via next(). You can check if
- * the end date is reached with the function hasNext(). After each step, you can
- * retrieve the current date via getCurrent().
- * The DataStep has scales ranging from milliseconds, seconds, minutes, hours,
- * days, to years.
- *
- * Version: 1.2
- *
- * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
- * or new Date(2010, 9, 21, 23, 45, 00)
- * @param {Date} [end] The end date
- * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
+ * A horizontal time axis
+ * @param {Object} [options] See DataAxis.setOptions for the available
+ * options.
+ * @constructor DataAxis
+ * @extends Component
+ * @param body
*/
- function DataStep(start, end, minimumStep, containerHeight, customRange, alignZeros) {
- // variables
- this.current = 0;
+ function DataAxis (body, options, svg, linegraphOptions) {
+ this.id = util.randomUUID();
+ this.body = body;
- this.autoScale = true;
- this.stepIndex = 0;
- this.step = 1;
- this.scale = 1;
+ this.defaultOptions = {
+ orientation: 'left', // supported: 'left', 'right'
+ showMinorLabels: true,
+ showMajorLabels: true,
+ showMinorLines: true,
+ showMajorLines: true,
+ icons: true,
+ majorLinesOffset: 7,
+ minorLinesOffset: 4,
+ labelOffsetX: 10,
+ labelOffsetY: 2,
+ iconWidth: 20,
+ width: '40px',
+ visible: true,
+ alignZeros: true,
+ customRange: {
+ left: {min:undefined, max:undefined},
+ right: {min:undefined, max:undefined}
+ },
+ title: {
+ left: {text:undefined},
+ right: {text:undefined}
+ },
+ format: {
+ left: {decimals: undefined},
+ right: {decimals: undefined}
+ }
+ };
- this.marginStart;
- this.marginEnd;
- this.deadSpace = 0;
+ this.linegraphOptions = linegraphOptions;
+ this.linegraphSVG = svg;
+ this.props = {};
+ this.DOMelements = { // dynamic elements
+ lines: {},
+ labels: {},
+ title: {}
+ };
- this.majorSteps = [1, 2, 5, 10];
- this.minorSteps = [0.25, 0.5, 1, 2];
+ this.dom = {};
- this.alignZeros = alignZeros;
+ this.range = {start:0, end:0};
- this.setRange(start, end, minimumStep, containerHeight, customRange);
- }
+ this.options = util.extend({}, this.defaultOptions);
+ this.conversionFactor = 1;
+ this.setOptions(options);
+ this.width = Number(('' + this.options.width).replace("px",""));
+ this.minWidth = this.width;
+ this.height = this.linegraphSVG.offsetHeight;
+ this.hidden = false;
+ this.stepPixels = 25;
+ this.stepPixelsForced = 25;
+ this.zeroCrossing = -1;
- /**
- * Set a new range
- * If minimumStep is provided, the step size is chosen as close as possible
- * to the minimumStep but larger than minimumStep. If minimumStep is not
- * provided, the scale is set to 1 DAY.
- * The minimumStep should correspond with the onscreen size of about 6 characters
- * @param {Number} [start] The start date and time.
- * @param {Number} [end] The end date and time.
- * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
- */
- DataStep.prototype.setRange = function(start, end, minimumStep, containerHeight, customRange) {
- this._start = customRange.min === undefined ? start : customRange.min;
- this._end = customRange.max === undefined ? end : customRange.max;
+ this.lineOffset = 0;
+ this.master = true;
+ this.svgElements = {};
+ this.iconsRemoved = false;
- if (this._start == this._end) {
- this._start -= 0.75;
- this._end += 1;
- }
- if (this.autoScale == true) {
- this.setMinimumStep(minimumStep, containerHeight);
- }
+ this.groups = {};
+ this.amountOfGroups = 0;
- this.setFirst(customRange);
- };
+ // create the HTML DOM
+ this._create();
- /**
- * Automatically determine the scale that bests fits the provided minimum step
- * @param {Number} [minimumStep] The minimum step size in milliseconds
- */
- DataStep.prototype.setMinimumStep = function(minimumStep, containerHeight) {
- // round to floor
- var size = this._end - this._start;
- var safeSize = size * 1.2;
- var minimumStepValue = minimumStep * (safeSize / containerHeight);
- var orderOfMagnitude = Math.round(Math.log(safeSize)/Math.LN10);
+ var me = this;
+ this.body.emitter.on("verticalDrag", function() {
+ me.dom.lineContainer.style.top = me.body.domProps.scrollTop + 'px';
+ });
+ }
- var minorStepIdx = -1;
- var magnitudefactor = Math.pow(10,orderOfMagnitude);
+ DataAxis.prototype = new Component();
- var start = 0;
- if (orderOfMagnitude < 0) {
- start = orderOfMagnitude;
- }
- var solutionFound = false;
- for (var i = start; Math.abs(i) <= Math.abs(orderOfMagnitude); i++) {
- magnitudefactor = Math.pow(10,i);
- for (var j = 0; j < this.minorSteps.length; j++) {
- var stepSize = magnitudefactor * this.minorSteps[j];
- if (stepSize >= minimumStepValue) {
- solutionFound = true;
- minorStepIdx = j;
- break;
- }
- }
- if (solutionFound == true) {
- break;
- }
+ DataAxis.prototype.addGroup = function(label, graphOptions) {
+ if (!this.groups.hasOwnProperty(label)) {
+ this.groups[label] = graphOptions;
}
- this.stepIndex = minorStepIdx;
- this.scale = magnitudefactor;
- this.step = magnitudefactor * this.minorSteps[minorStepIdx];
+ this.amountOfGroups += 1;
};
+ DataAxis.prototype.updateGroup = function(label, graphOptions) {
+ this.groups[label] = graphOptions;
+ };
-
- /**
- * Round the current date to the first minor date value
- * This must be executed once when the current date is set to start Date
- */
- DataStep.prototype.setFirst = function(customRange) {
- if (customRange === undefined) {
- customRange = {};
- }
-
- var niceStart = customRange.min === undefined ? this._start - (this.scale * 2 * this.minorSteps[this.stepIndex]) : customRange.min;
- var niceEnd = customRange.max === undefined ? this._end + (this.scale * this.minorSteps[this.stepIndex]) : customRange.max;
-
- this.marginEnd = customRange.max === undefined ? this.roundToMinor(niceEnd) : customRange.max;
- this.marginStart = customRange.min === undefined ? this.roundToMinor(niceStart) : customRange.min;
-
- // if we need to align the zero's we need to make sure that there is a zero to use.
- if (this.alignZeros == true && (this.marginEnd - this.marginStart) % this.step != 0) {
- this.marginEnd += this.marginEnd % this.step;
+ DataAxis.prototype.removeGroup = function(label) {
+ if (this.groups.hasOwnProperty(label)) {
+ delete this.groups[label];
+ this.amountOfGroups -= 1;
}
+ };
- this.deadSpace = this.roundToMinor(niceEnd) - niceEnd + this.roundToMinor(niceStart) - niceStart;
- this.marginRange = this.marginEnd - this.marginStart;
+ DataAxis.prototype.setOptions = function (options) {
+ if (options) {
+ var redraw = false;
+ if (this.options.orientation != options.orientation && options.orientation !== undefined) {
+ redraw = true;
+ }
+ var fields = [
+ 'orientation',
+ 'showMinorLabels',
+ 'showMajorLabels',
+ 'showMajorLines',
+ 'showMinorLines',
+ 'icons',
+ 'majorLinesOffset',
+ 'minorLinesOffset',
+ 'labelOffsetX',
+ 'labelOffsetY',
+ 'iconWidth',
+ 'width',
+ 'visible',
+ 'customRange',
+ 'title',
+ 'format',
+ 'alignZeros'
+ ];
+ util.selectiveExtend(fields, this.options, options);
- this.current = this.marginEnd;
- };
+ this.minWidth = Number(('' + this.options.width).replace("px",""));
- DataStep.prototype.roundToMinor = function(value) {
- var rounded = value - (value % (this.scale * this.minorSteps[this.stepIndex]));
- if (value % (this.scale * this.minorSteps[this.stepIndex]) > 0.5 * (this.scale * this.minorSteps[this.stepIndex])) {
- return rounded + (this.scale * this.minorSteps[this.stepIndex]);
- }
- else {
- return rounded;
+ if (redraw == true && this.dom.frame) {
+ this.hide();
+ this.show();
+ }
}
- }
+ };
/**
- * Check if the there is a next step
- * @return {boolean} true if the current date has not passed the end date
+ * Create the HTML DOM for the DataAxis
*/
- DataStep.prototype.hasNext = function () {
- return (this.current >= this.marginStart);
- };
+ DataAxis.prototype._create = function() {
+ this.dom.frame = document.createElement('div');
+ this.dom.frame.style.width = this.options.width;
+ this.dom.frame.style.height = this.height;
- /**
- * Do the next step
- */
- DataStep.prototype.next = function() {
- var prev = this.current;
- this.current -= this.step;
-
- // safety mechanism: if current time is still unchanged, move to the end
- if (this.current == prev) {
- this.current = this._end;
- }
- };
+ this.dom.lineContainer = document.createElement('div');
+ this.dom.lineContainer.style.width = '100%';
+ this.dom.lineContainer.style.height = this.height;
+ this.dom.lineContainer.style.position = 'relative';
- /**
- * Do the next step
- */
- DataStep.prototype.previous = function() {
- this.current += this.step;
- this.marginEnd += this.step;
- this.marginRange = this.marginEnd - this.marginStart;
+ // create svg element for graph drawing.
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
+ this.svg.style.position = "absolute";
+ this.svg.style.top = '0px';
+ this.svg.style.height = '100%';
+ this.svg.style.width = '100%';
+ this.svg.style.display = "block";
+ this.dom.frame.appendChild(this.svg);
};
+ DataAxis.prototype._redrawGroupIcons = function () {
+ DOMutil.prepareElements(this.svgElements);
+ var x;
+ var iconWidth = this.options.iconWidth;
+ var iconHeight = 15;
+ var iconOffset = 4;
+ var y = iconOffset + 0.5 * iconHeight;
- /**
- * Get the current datetime
- * @return {String} current The current date
- */
- DataStep.prototype.getCurrent = function(decimals) {
- // prevent round-off errors when close to zero
- var current = (Math.abs(this.current) < this.step / 2) ? 0 : this.current;
- var toPrecision = '' + Number(current).toPrecision(5);
-
- // If decimals is specified, then limit or extend the string as required
- if(decimals !== undefined && !isNaN(Number(decimals))) {
- // If string includes exponent, then we need to add it to the end
- var exp = "";
- var index = toPrecision.indexOf("e");
- if(index != -1) {
- // Get the exponent
- exp = toPrecision.slice(index);
- // Remove the exponent in case we need to zero-extend
- toPrecision = toPrecision.slice(0, index);
- }
- index = Math.max(toPrecision.indexOf(","), toPrecision.indexOf("."));
- if(index === -1) {
- // No decimal found - if we want decimals, then we need to add it
- if(decimals !== 0) {
- toPrecision += '.';
- }
- // Calculate how long the string should be
- index = toPrecision.length + decimals;
- }
- else if(decimals !== 0) {
- // Calculate how long the string should be - accounting for the decimal place
- index += decimals + 1;
- }
- if(index > toPrecision.length) {
- // We need to add zeros!
- for(var cnt = index - toPrecision.length; cnt > 0; cnt--) {
- toPrecision += '0';
- }
- }
- else {
- // we need to remove characters
- toPrecision = toPrecision.slice(0, index);
- }
- // Add the exponent if there is one
- toPrecision += exp;
+ if (this.options.orientation == 'left') {
+ x = iconOffset;
}
else {
- if (toPrecision.indexOf(",") != -1 || toPrecision.indexOf(".") != -1) {
- // If no decimal is specified, and there are decimal places, remove trailing zeros
- for (var i = toPrecision.length - 1; i > 0; i--) {
- if (toPrecision[i] == "0") {
- toPrecision = toPrecision.slice(0, i);
- }
- else if (toPrecision[i] == "." || toPrecision[i] == ",") {
- toPrecision = toPrecision.slice(0, i);
- break;
- }
- else {
- break;
- }
+ x = this.width - iconWidth - iconOffset;
+ }
+
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
+ this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight);
+ y += iconHeight + iconOffset;
}
}
}
- return toPrecision;
+ DOMutil.cleanupElements(this.svgElements);
+ this.iconsRemoved = false;
};
-
+ DataAxis.prototype._cleanupIcons = function() {
+ if (this.iconsRemoved == false) {
+ DOMutil.prepareElements(this.svgElements);
+ DOMutil.cleanupElements(this.svgElements);
+ this.iconsRemoved = true;
+ }
+ }
/**
- * Snap a date to a rounded value.
- * The snap intervals are dependent on the current scale and step.
- * @param {Date} date the date to be snapped.
- * @return {Date} snappedDate
+ * Create the HTML DOM for the DataAxis
*/
- DataStep.prototype.snap = function(date) {
+ DataAxis.prototype.show = function() {
+ this.hidden = false;
+ if (!this.dom.frame.parentNode) {
+ if (this.options.orientation == 'left') {
+ this.body.dom.left.appendChild(this.dom.frame);
+ }
+ else {
+ this.body.dom.right.appendChild(this.dom.frame);
+ }
+ }
+ if (!this.dom.lineContainer.parentNode) {
+ this.body.dom.backgroundHorizontal.appendChild(this.dom.lineContainer);
+ }
};
/**
- * Check if the current value is a major value (for example when the step
- * is DAY, a major value is each first day of the MONTH)
- * @return {boolean} true if current date is major, else false.
+ * Create the HTML DOM for the DataAxis
*/
- DataStep.prototype.isMajor = function() {
- return (this.current % (this.scale * this.majorSteps[this.stepIndex]) == 0);
- };
-
- module.exports = DataStep;
-
+ DataAxis.prototype.hide = function() {
+ this.hidden = true;
+ if (this.dom.frame.parentNode) {
+ this.dom.frame.parentNode.removeChild(this.dom.frame);
+ }
-/***/ },
-/* 45 */
-/***/ function(module, exports, __webpack_require__) {
+ if (this.dom.lineContainer.parentNode) {
+ this.dom.lineContainer.parentNode.removeChild(this.dom.lineContainer);
+ }
+ };
- var util = __webpack_require__(1);
- var DOMutil = __webpack_require__(6);
- var Component = __webpack_require__(23);
- var DataStep = __webpack_require__(44);
+ /**
+ * Set a range (start and end)
+ * @param end
+ * @param start
+ * @param end
+ */
+ DataAxis.prototype.setRange = function (start, end) {
+ if (this.master == false && this.options.alignZeros == true && this.zeroCrossing != -1) {
+ if (start > 0) {
+ start = 0;
+ }
+ }
+ this.range.start = start;
+ this.range.end = end;
+ };
/**
- * A horizontal time axis
- * @param {Object} [options] See DataAxis.setOptions for the available
- * options.
- * @constructor DataAxis
- * @extends Component
- * @param body
+ * Repaint the component
+ * @return {boolean} Returns true if the component is resized
*/
- function DataAxis (body, options, svg, linegraphOptions) {
- this.id = util.randomUUID();
- this.body = body;
+ DataAxis.prototype.redraw = function () {
+ var resized = false;
+ var activeGroups = 0;
+
+ // Make sure the line container adheres to the vertical scrolling.
+ this.dom.lineContainer.style.top = this.body.domProps.scrollTop + 'px';
- this.defaultOptions = {
- orientation: 'left', // supported: 'left', 'right'
- showMinorLabels: true,
- showMajorLabels: true,
- showMinorLines: true,
- showMajorLines: true,
- icons: true,
- majorLinesOffset: 7,
- minorLinesOffset: 4,
- labelOffsetX: 10,
- labelOffsetY: 2,
- iconWidth: 20,
- width: '40px',
- visible: true,
- alignZeros: true,
- customRange: {
- left: {min:undefined, max:undefined},
- right: {min:undefined, max:undefined}
- },
- title: {
- left: {text:undefined},
- right: {text:undefined}
- },
- format: {
- left: {decimals: undefined},
- right: {decimals: undefined}
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
+ activeGroups++;
+ }
}
- };
+ }
+ if (this.amountOfGroups == 0 || activeGroups == 0) {
+ this.hide();
+ }
+ else {
+ this.show();
+ this.height = Number(this.linegraphSVG.style.height.replace("px",""));
- this.linegraphOptions = linegraphOptions;
- this.linegraphSVG = svg;
- this.props = {};
- this.DOMelements = { // dynamic elements
- lines: {},
- labels: {},
- title: {}
- };
+ // svg offsetheight did not work in firefox and explorer...
+ this.dom.lineContainer.style.height = this.height + 'px';
+ this.width = this.options.visible == true ? Number(('' + this.options.width).replace("px","")) : 0;
- this.dom = {};
+ var props = this.props;
+ var frame = this.dom.frame;
- this.range = {start:0, end:0};
+ // update classname
+ frame.className = 'dataaxis';
- this.options = util.extend({}, this.defaultOptions);
- this.conversionFactor = 1;
-
- this.setOptions(options);
- this.width = Number(('' + this.options.width).replace("px",""));
- this.minWidth = this.width;
- this.height = this.linegraphSVG.offsetHeight;
- this.hidden = false;
-
- this.stepPixels = 25;
- this.stepPixelsForced = 25;
- this.zeroCrossing = -1;
-
- this.lineOffset = 0;
- this.master = true;
- this.svgElements = {};
- this.iconsRemoved = false;
-
-
- this.groups = {};
- this.amountOfGroups = 0;
-
- // create the HTML DOM
- this._create();
-
- var me = this;
- this.body.emitter.on("verticalDrag", function() {
- me.dom.lineContainer.style.top = me.body.domProps.scrollTop + 'px';
- });
- }
-
- DataAxis.prototype = new Component();
-
-
- DataAxis.prototype.addGroup = function(label, graphOptions) {
- if (!this.groups.hasOwnProperty(label)) {
- this.groups[label] = graphOptions;
- }
- this.amountOfGroups += 1;
- };
-
- DataAxis.prototype.updateGroup = function(label, graphOptions) {
- this.groups[label] = graphOptions;
- };
-
- DataAxis.prototype.removeGroup = function(label) {
- if (this.groups.hasOwnProperty(label)) {
- delete this.groups[label];
- this.amountOfGroups -= 1;
- }
- };
-
-
- DataAxis.prototype.setOptions = function (options) {
- if (options) {
- var redraw = false;
- if (this.options.orientation != options.orientation && options.orientation !== undefined) {
- redraw = true;
- }
- var fields = [
- 'orientation',
- 'showMinorLabels',
- 'showMajorLabels',
- 'showMajorLines',
- 'showMinorLines',
- 'icons',
- 'majorLinesOffset',
- 'minorLinesOffset',
- 'labelOffsetX',
- 'labelOffsetY',
- 'iconWidth',
- 'width',
- 'visible',
- 'customRange',
- 'title',
- 'format',
- 'alignZeros'
- ];
- util.selectiveExtend(fields, this.options, options);
-
- this.minWidth = Number(('' + this.options.width).replace("px",""));
-
- if (redraw == true && this.dom.frame) {
- this.hide();
- this.show();
- }
- }
- };
-
-
- /**
- * Create the HTML DOM for the DataAxis
- */
- DataAxis.prototype._create = function() {
- this.dom.frame = document.createElement('div');
- this.dom.frame.style.width = this.options.width;
- this.dom.frame.style.height = this.height;
-
- this.dom.lineContainer = document.createElement('div');
- this.dom.lineContainer.style.width = '100%';
- this.dom.lineContainer.style.height = this.height;
- this.dom.lineContainer.style.position = 'relative';
-
- // create svg element for graph drawing.
- this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
- this.svg.style.position = "absolute";
- this.svg.style.top = '0px';
- this.svg.style.height = '100%';
- this.svg.style.width = '100%';
- this.svg.style.display = "block";
- this.dom.frame.appendChild(this.svg);
- };
-
- DataAxis.prototype._redrawGroupIcons = function () {
- DOMutil.prepareElements(this.svgElements);
-
- var x;
- var iconWidth = this.options.iconWidth;
- var iconHeight = 15;
- var iconOffset = 4;
- var y = iconOffset + 0.5 * iconHeight;
-
- if (this.options.orientation == 'left') {
- x = iconOffset;
- }
- else {
- x = this.width - iconWidth - iconOffset;
- }
-
- for (var groupId in this.groups) {
- if (this.groups.hasOwnProperty(groupId)) {
- if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
- this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight);
- y += iconHeight + iconOffset;
- }
- }
- }
-
- DOMutil.cleanupElements(this.svgElements);
- this.iconsRemoved = false;
- };
-
- DataAxis.prototype._cleanupIcons = function() {
- if (this.iconsRemoved == false) {
- DOMutil.prepareElements(this.svgElements);
- DOMutil.cleanupElements(this.svgElements);
- this.iconsRemoved = true;
- }
- }
-
- /**
- * Create the HTML DOM for the DataAxis
- */
- DataAxis.prototype.show = function() {
- this.hidden = false;
- if (!this.dom.frame.parentNode) {
- if (this.options.orientation == 'left') {
- this.body.dom.left.appendChild(this.dom.frame);
- }
- else {
- this.body.dom.right.appendChild(this.dom.frame);
- }
- }
-
- if (!this.dom.lineContainer.parentNode) {
- this.body.dom.backgroundHorizontal.appendChild(this.dom.lineContainer);
- }
- };
-
- /**
- * Create the HTML DOM for the DataAxis
- */
- DataAxis.prototype.hide = function() {
- this.hidden = true;
- if (this.dom.frame.parentNode) {
- this.dom.frame.parentNode.removeChild(this.dom.frame);
- }
-
- if (this.dom.lineContainer.parentNode) {
- this.dom.lineContainer.parentNode.removeChild(this.dom.lineContainer);
- }
- };
-
- /**
- * Set a range (start and end)
- * @param end
- * @param start
- * @param end
- */
- DataAxis.prototype.setRange = function (start, end) {
- if (this.master == false && this.options.alignZeros == true && this.zeroCrossing != -1) {
- if (start > 0) {
- start = 0;
- }
- }
- this.range.start = start;
- this.range.end = end;
- };
-
- /**
- * Repaint the component
- * @return {boolean} Returns true if the component is resized
- */
- DataAxis.prototype.redraw = function () {
- var resized = false;
- var activeGroups = 0;
-
- // Make sure the line container adheres to the vertical scrolling.
- this.dom.lineContainer.style.top = this.body.domProps.scrollTop + 'px';
-
- for (var groupId in this.groups) {
- if (this.groups.hasOwnProperty(groupId)) {
- if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
- activeGroups++;
- }
- }
- }
- if (this.amountOfGroups == 0 || activeGroups == 0) {
- this.hide();
- }
- else {
- this.show();
- this.height = Number(this.linegraphSVG.style.height.replace("px",""));
-
- // svg offsetheight did not work in firefox and explorer...
- this.dom.lineContainer.style.height = this.height + 'px';
- this.width = this.options.visible == true ? Number(('' + this.options.width).replace("px","")) : 0;
-
- var props = this.props;
- var frame = this.dom.frame;
-
- // update classname
- frame.className = 'dataaxis';
-
- // calculate character width and height
- this._calculateCharSize();
+ // calculate character width and height
+ this._calculateCharSize();
var orientation = this.options.orientation;
var showMinorLabels = this.options.showMinorLabels;
@@ -21565,484 +21284,493 @@ return /******/ (function(modules) { // webpackBootstrap
/***/ },
-/* 46 */
+/* 45 */
/***/ function(module, exports, __webpack_require__) {
- var util = __webpack_require__(1);
- var DOMutil = __webpack_require__(6);
- var Line = __webpack_require__(47);
- var Bar = __webpack_require__(49);
- var Points = __webpack_require__(48);
-
/**
- * /**
- * @param {object} group | the object of the group from the dataset
- * @param {string} groupId | ID of the group
- * @param {object} options | the default options
- * @param {array} groupsUsingDefaultStyles | this array has one entree.
- * It is passed as an array so it is passed by reference.
- * It enumerates through the default styles
- * @constructor
- */
- function GraphGroup (group, groupId, options, groupsUsingDefaultStyles) {
- this.id = groupId;
- var fields = ['sampling','style','sort','yAxisOrientation','barChart','drawPoints','shaded','catmullRom']
- this.options = util.selectiveBridgeObject(fields,options);
- this.usingDefaultStyle = group.className === undefined;
- this.groupsUsingDefaultStyles = groupsUsingDefaultStyles;
- this.zeroPosition = 0;
- this.update(group);
- if (this.usingDefaultStyle == true) {
- this.groupsUsingDefaultStyles[0] += 1;
- }
- this.itemsData = [];
- this.visible = group.visible === undefined ? true : group.visible;
- }
+ * @constructor DataStep
+ * The class DataStep is an iterator for data for the lineGraph. You provide a start data point and an
+ * end data point. The class itself determines the best scale (step size) based on the
+ * provided start Date, end Date, and minimumStep.
+ *
+ * If minimumStep is provided, the step size is chosen as close as possible
+ * to the minimumStep but larger than minimumStep. If minimumStep is not
+ * provided, the scale is set to 1 DAY.
+ * The minimumStep should correspond with the onscreen size of about 6 characters
+ *
+ * Alternatively, you can set a scale by hand.
+ * After creation, you can initialize the class by executing first(). Then you
+ * can iterate from the start date to the end date via next(). You can check if
+ * the end date is reached with the function hasNext(). After each step, you can
+ * retrieve the current date via getCurrent().
+ * The DataStep has scales ranging from milliseconds, seconds, minutes, hours,
+ * days, to years.
+ *
+ * Version: 1.2
+ *
+ * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
+ * or new Date(2010, 9, 21, 23, 45, 00)
+ * @param {Date} [end] The end date
+ * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
+ */
+ function DataStep(start, end, minimumStep, containerHeight, customRange, alignZeros) {
+ // variables
+ this.current = 0;
+ this.autoScale = true;
+ this.stepIndex = 0;
+ this.step = 1;
+ this.scale = 1;
- /**
- * this loads a reference to all items in this group into this group.
- * @param {array} items
- */
- GraphGroup.prototype.setItems = function(items) {
- if (items != null) {
- this.itemsData = items;
- if (this.options.sort == true) {
- this.itemsData.sort(function (a,b) {return a.x - b.x;})
- }
- }
- else {
- this.itemsData = [];
- }
- };
+ this.marginStart;
+ this.marginEnd;
+ this.deadSpace = 0;
+ this.majorSteps = [1, 2, 5, 10];
+ this.minorSteps = [0.25, 0.5, 1, 2];
+
+ this.alignZeros = alignZeros;
+
+ this.setRange(start, end, minimumStep, containerHeight, customRange);
+ }
- /**
- * this is used for plotting barcharts, this way, we only have to calculate it once.
- * @param pos
- */
- GraphGroup.prototype.setZeroPosition = function(pos) {
- this.zeroPosition = pos;
- };
/**
- * set the options of the graph group over the default options.
- * @param options
+ * Set a new range
+ * If minimumStep is provided, the step size is chosen as close as possible
+ * to the minimumStep but larger than minimumStep. If minimumStep is not
+ * provided, the scale is set to 1 DAY.
+ * The minimumStep should correspond with the onscreen size of about 6 characters
+ * @param {Number} [start] The start date and time.
+ * @param {Number} [end] The end date and time.
+ * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
*/
- GraphGroup.prototype.setOptions = function(options) {
- if (options !== undefined) {
- var fields = ['sampling','style','sort','yAxisOrientation','barChart'];
- util.selectiveDeepExtend(fields, this.options, options);
-
- util.mergeOptions(this.options, options,'catmullRom');
- util.mergeOptions(this.options, options,'drawPoints');
- util.mergeOptions(this.options, options,'shaded');
+ DataStep.prototype.setRange = function(start, end, minimumStep, containerHeight, customRange) {
+ this._start = customRange.min === undefined ? start : customRange.min;
+ this._end = customRange.max === undefined ? end : customRange.max;
- if (options.catmullRom) {
- if (typeof options.catmullRom == 'object') {
- if (options.catmullRom.parametrization) {
- if (options.catmullRom.parametrization == 'uniform') {
- this.options.catmullRom.alpha = 0;
- }
- else if (options.catmullRom.parametrization == 'chordal') {
- this.options.catmullRom.alpha = 1.0;
- }
- else {
- this.options.catmullRom.parametrization = 'centripetal';
- this.options.catmullRom.alpha = 0.5;
- }
- }
- }
- }
+ if (this._start == this._end) {
+ this._start -= 0.75;
+ this._end += 1;
}
- if (this.options.style == 'line') {
- this.type = new Line(this.id, this.options);
- }
- else if (this.options.style == 'bar') {
- this.type = new Bar(this.id, this.options);
- }
- else if (this.options.style == 'points') {
- this.type = new Points(this.id, this.options);
+ if (this.autoScale == true) {
+ this.setMinimumStep(minimumStep, containerHeight);
}
- };
-
- /**
- * this updates the current group class with the latest group dataset entree, used in _updateGroup in linegraph
- * @param group
- */
- GraphGroup.prototype.update = function(group) {
- this.group = group;
- this.content = group.content || 'graph';
- this.className = group.className || this.className || "graphGroup" + this.groupsUsingDefaultStyles[0] % 10;
- this.visible = group.visible === undefined ? true : group.visible;
- this.style = group.style;
- this.setOptions(group.options);
+ this.setFirst(customRange);
};
-
/**
- * draw the icon for the legend.
- *
- * @param x
- * @param y
- * @param JSONcontainer
- * @param SVGcontainer
- * @param iconWidth
- * @param iconHeight
+ * Automatically determine the scale that bests fits the provided minimum step
+ * @param {Number} [minimumStep] The minimum step size in milliseconds
*/
- GraphGroup.prototype.drawIcon = function(x, y, JSONcontainer, SVGcontainer, iconWidth, iconHeight) {
- var fillHeight = iconHeight * 0.5;
- var path, fillPath;
+ DataStep.prototype.setMinimumStep = function(minimumStep, containerHeight) {
+ // round to floor
+ var size = this._end - this._start;
+ var safeSize = size * 1.2;
+ var minimumStepValue = minimumStep * (safeSize / containerHeight);
+ var orderOfMagnitude = Math.round(Math.log(safeSize)/Math.LN10);
- var outline = DOMutil.getSVGElement("rect", JSONcontainer, SVGcontainer);
- outline.setAttributeNS(null, "x", x);
- outline.setAttributeNS(null, "y", y - fillHeight);
- outline.setAttributeNS(null, "width", iconWidth);
- outline.setAttributeNS(null, "height", 2*fillHeight);
- outline.setAttributeNS(null, "class", "outline");
+ var minorStepIdx = -1;
+ var magnitudefactor = Math.pow(10,orderOfMagnitude);
- if (this.options.style == 'line') {
- path = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer);
- path.setAttributeNS(null, "class", this.className);
- if(this.style !== undefined) {
- path.setAttributeNS(null, "style", this.style);
- }
+ var start = 0;
+ if (orderOfMagnitude < 0) {
+ start = orderOfMagnitude;
+ }
- path.setAttributeNS(null, "d", "M" + x + ","+y+" L" + (x + iconWidth) + ","+y+"");
- if (this.options.shaded.enabled == true) {
- fillPath = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer);
- if (this.options.shaded.orientation == 'top') {
- fillPath.setAttributeNS(null, "d", "M"+x+", " + (y - fillHeight) +
- "L"+x+","+y+" L"+ (x + iconWidth) + ","+y+" L"+ (x + iconWidth) + "," + (y - fillHeight));
- }
- else {
- fillPath.setAttributeNS(null, "d", "M"+x+","+y+" " +
- "L"+x+"," + (y + fillHeight) + " " +
- "L"+ (x + iconWidth) + "," + (y + fillHeight) +
- "L"+ (x + iconWidth) + ","+y);
+ var solutionFound = false;
+ for (var i = start; Math.abs(i) <= Math.abs(orderOfMagnitude); i++) {
+ magnitudefactor = Math.pow(10,i);
+ for (var j = 0; j < this.minorSteps.length; j++) {
+ var stepSize = magnitudefactor * this.minorSteps[j];
+ if (stepSize >= minimumStepValue) {
+ solutionFound = true;
+ minorStepIdx = j;
+ break;
}
- fillPath.setAttributeNS(null, "class", this.className + " iconFill");
}
-
- if (this.options.drawPoints.enabled == true) {
- DOMutil.drawPoint(x + 0.5 * iconWidth,y, this, JSONcontainer, SVGcontainer);
+ if (solutionFound == true) {
+ break;
}
}
- else {
- var barWidth = Math.round(0.3 * iconWidth);
- var bar1Height = Math.round(0.4 * iconHeight);
- var bar2Height = Math.round(0.75 * iconHeight);
-
- var offset = Math.round((iconWidth - (2 * barWidth))/3);
-
- DOMutil.drawBar(x + 0.5*barWidth + offset , y + fillHeight - bar1Height - 1, barWidth, bar1Height, this.className + ' bar', JSONcontainer, SVGcontainer);
- DOMutil.drawBar(x + 1.5*barWidth + offset + 2, y + fillHeight - bar2Height - 1, barWidth, bar2Height, this.className + ' bar', JSONcontainer, SVGcontainer);
- }
+ this.stepIndex = minorStepIdx;
+ this.scale = magnitudefactor;
+ this.step = magnitudefactor * this.minorSteps[minorStepIdx];
};
+
/**
- * return the legend entree for this group.
- *
- * @param iconWidth
- * @param iconHeight
- * @returns {{icon: HTMLElement, label: (group.content|*|string), orientation: (.options.yAxisOrientation|*)}}
+ * Round the current date to the first minor date value
+ * This must be executed once when the current date is set to start Date
*/
- GraphGroup.prototype.getLegend = function(iconWidth, iconHeight) {
- var svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
- this.drawIcon(0,0.5*iconHeight,[],svg,iconWidth,iconHeight);
- return {icon: svg, label: this.content, orientation:this.options.yAxisOrientation};
- }
-
- GraphGroup.prototype.getYRange = function(groupData) {
- return this.type.getYRange(groupData);
- }
+ DataStep.prototype.setFirst = function(customRange) {
+ if (customRange === undefined) {
+ customRange = {};
+ }
- GraphGroup.prototype.draw = function(dataset, group, framework) {
- this.type.draw(dataset, group, framework);
- }
+ var niceStart = customRange.min === undefined ? this._start - (this.scale * 2 * this.minorSteps[this.stepIndex]) : customRange.min;
+ var niceEnd = customRange.max === undefined ? this._end + (this.scale * this.minorSteps[this.stepIndex]) : customRange.max;
+ this.marginEnd = customRange.max === undefined ? this.roundToMinor(niceEnd) : customRange.max;
+ this.marginStart = customRange.min === undefined ? this.roundToMinor(niceStart) : customRange.min;
- module.exports = GraphGroup;
+ // if we need to align the zero's we need to make sure that there is a zero to use.
+ if (this.alignZeros == true && (this.marginEnd - this.marginStart) % this.step != 0) {
+ this.marginEnd += this.marginEnd % this.step;
+ }
+ this.deadSpace = this.roundToMinor(niceEnd) - niceEnd + this.roundToMinor(niceStart) - niceStart;
+ this.marginRange = this.marginEnd - this.marginStart;
-/***/ },
-/* 47 */
-/***/ function(module, exports, __webpack_require__) {
- /**
- * Created by Alex on 11/11/2014.
- */
- var DOMutil = __webpack_require__(6);
- var Points = __webpack_require__(48);
+ this.current = this.marginEnd;
+ };
- function Line(groupId, options) {
- this.groupId = groupId;
- this.options = options;
+ DataStep.prototype.roundToMinor = function(value) {
+ var rounded = value - (value % (this.scale * this.minorSteps[this.stepIndex]));
+ if (value % (this.scale * this.minorSteps[this.stepIndex]) > 0.5 * (this.scale * this.minorSteps[this.stepIndex])) {
+ return rounded + (this.scale * this.minorSteps[this.stepIndex]);
+ }
+ else {
+ return rounded;
+ }
}
- Line.prototype.getYRange = function(groupData) {
- var yMin = groupData[0].y;
- var yMax = groupData[0].y;
- for (var j = 0; j < groupData.length; j++) {
- yMin = yMin > groupData[j].y ? groupData[j].y : yMin;
- yMax = yMax < groupData[j].y ? groupData[j].y : yMax;
+
+ /**
+ * Check if the there is a next step
+ * @return {boolean} true if the current date has not passed the end date
+ */
+ DataStep.prototype.hasNext = function () {
+ return (this.current >= this.marginStart);
+ };
+
+ /**
+ * Do the next step
+ */
+ DataStep.prototype.next = function() {
+ var prev = this.current;
+ this.current -= this.step;
+
+ // safety mechanism: if current time is still unchanged, move to the end
+ if (this.current == prev) {
+ this.current = this._end;
}
- return {min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation};
};
+ /**
+ * Do the next step
+ */
+ DataStep.prototype.previous = function() {
+ this.current += this.step;
+ this.marginEnd += this.step;
+ this.marginRange = this.marginEnd - this.marginStart;
+ };
+
+
/**
- * draw a line graph
- *
- * @param dataset
- * @param group
+ * Get the current datetime
+ * @return {String} current The current date
*/
- Line.prototype.draw = function (dataset, group, framework) {
- if (dataset != null) {
- if (dataset.length > 0) {
- var path, d;
- var svgHeight = Number(framework.svg.style.height.replace('px',''));
- path = DOMutil.getSVGElement('path', framework.svgElements, framework.svg);
- path.setAttributeNS(null, "class", group.className);
- if(group.style !== undefined) {
- path.setAttributeNS(null, "style", group.style);
- }
+ DataStep.prototype.getCurrent = function(decimals) {
+ // prevent round-off errors when close to zero
+ var current = (Math.abs(this.current) < this.step / 2) ? 0 : this.current;
+ var toPrecision = '' + Number(current).toPrecision(5);
- // construct path from dataset
- if (group.options.catmullRom.enabled == true) {
- d = Line._catmullRom(dataset, group);
+ // If decimals is specified, then limit or extend the string as required
+ if(decimals !== undefined && !isNaN(Number(decimals))) {
+ // If string includes exponent, then we need to add it to the end
+ var exp = "";
+ var index = toPrecision.indexOf("e");
+ if(index != -1) {
+ // Get the exponent
+ exp = toPrecision.slice(index);
+ // Remove the exponent in case we need to zero-extend
+ toPrecision = toPrecision.slice(0, index);
+ }
+ index = Math.max(toPrecision.indexOf(","), toPrecision.indexOf("."));
+ if(index === -1) {
+ // No decimal found - if we want decimals, then we need to add it
+ if(decimals !== 0) {
+ toPrecision += '.';
}
- else {
- d = Line._linear(dataset);
+ // Calculate how long the string should be
+ index = toPrecision.length + decimals;
+ }
+ else if(decimals !== 0) {
+ // Calculate how long the string should be - accounting for the decimal place
+ index += decimals + 1;
+ }
+ if(index > toPrecision.length) {
+ // We need to add zeros!
+ for(var cnt = index - toPrecision.length; cnt > 0; cnt--) {
+ toPrecision += '0';
}
-
- // append with points for fill and finalize the path
- if (group.options.shaded.enabled == true) {
- var fillPath = DOMutil.getSVGElement('path', framework.svgElements, framework.svg);
- var dFill;
- if (group.options.shaded.orientation == 'top') {
- dFill = 'M' + dataset[0].x + ',' + 0 + ' ' + d + 'L' + dataset[dataset.length - 1].x + ',' + 0;
+ }
+ else {
+ // we need to remove characters
+ toPrecision = toPrecision.slice(0, index);
+ }
+ // Add the exponent if there is one
+ toPrecision += exp;
+ }
+ else {
+ if (toPrecision.indexOf(",") != -1 || toPrecision.indexOf(".") != -1) {
+ // If no decimal is specified, and there are decimal places, remove trailing zeros
+ for (var i = toPrecision.length - 1; i > 0; i--) {
+ if (toPrecision[i] == "0") {
+ toPrecision = toPrecision.slice(0, i);
}
- else {
- dFill = 'M' + dataset[0].x + ',' + svgHeight + ' ' + d + 'L' + dataset[dataset.length - 1].x + ',' + svgHeight;
+ else if (toPrecision[i] == "." || toPrecision[i] == ",") {
+ toPrecision = toPrecision.slice(0, i);
+ break;
}
- fillPath.setAttributeNS(null, "class", group.className + " fill");
- if(group.options.shaded.style !== undefined) {
- fillPath.setAttributeNS(null, "style", group.options.shaded.style);
+ else {
+ break;
}
- fillPath.setAttributeNS(null, "d", dFill);
- }
- // copy properties to path for drawing.
- path.setAttributeNS(null, 'd', 'M' + d);
-
- // draw points
- if (group.options.drawPoints.enabled == true) {
- Points.draw(dataset, group, framework);
}
}
}
+
+ return toPrecision;
};
/**
- * This uses an uniform parametrization of the CatmullRom algorithm:
- * 'On the Parameterization of Catmull-Rom Curves' by Cem Yuksel et al.
- * @param data
- * @returns {string}
- * @private
+ * Snap a date to a rounded value.
+ * The snap intervals are dependent on the current scale and step.
+ * @param {Date} date the date to be snapped.
+ * @return {Date} snappedDate
*/
- Line._catmullRomUniform = function(data) {
- // catmull rom
- var p0, p1, p2, p3, bp1, bp2;
- var d = Math.round(data[0].x) + ',' + Math.round(data[0].y) + ' ';
- var normalization = 1/6;
- var length = data.length;
- for (var i = 0; i < length - 1; i++) {
+ DataStep.prototype.snap = function(date) {
- p0 = (i == 0) ? data[0] : data[i-1];
- p1 = data[i];
- p2 = data[i+1];
- p3 = (i + 2 < length) ? data[i+2] : p2;
+ };
+ /**
+ * Check if the current value is a major value (for example when the step
+ * is DAY, a major value is each first day of the MONTH)
+ * @return {boolean} true if current date is major, else false.
+ */
+ DataStep.prototype.isMajor = function() {
+ return (this.current % (this.scale * this.majorSteps[this.stepIndex]) == 0);
+ };
- // Catmull-Rom to Cubic Bezier conversion matrix
- // 0 1 0 0
- // -1/6 1 1/6 0
- // 0 1/6 1 -1/6
- // 0 0 1 0
+ module.exports = DataStep;
- // bp0 = { x: p1.x, y: p1.y };
- bp1 = { x: ((-p0.x + 6*p1.x + p2.x) *normalization), y: ((-p0.y + 6*p1.y + p2.y) *normalization)};
- bp2 = { x: (( p1.x + 6*p2.x - p3.x) *normalization), y: (( p1.y + 6*p2.y - p3.y) *normalization)};
- // bp0 = { x: p2.x, y: p2.y };
- d += 'C' +
- bp1.x + ',' +
- bp1.y + ' ' +
- bp2.x + ',' +
- bp2.y + ' ' +
- p2.x + ',' +
- p2.y + ' ';
+/***/ },
+/* 46 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var util = __webpack_require__(1);
+ var DOMutil = __webpack_require__(6);
+ var Line = __webpack_require__(47);
+ var Bar = __webpack_require__(49);
+ var Points = __webpack_require__(48);
+
+ /**
+ * /**
+ * @param {object} group | the object of the group from the dataset
+ * @param {string} groupId | ID of the group
+ * @param {object} options | the default options
+ * @param {array} groupsUsingDefaultStyles | this array has one entree.
+ * It is passed as an array so it is passed by reference.
+ * It enumerates through the default styles
+ * @constructor
+ */
+ function GraphGroup (group, groupId, options, groupsUsingDefaultStyles) {
+ this.id = groupId;
+ var fields = ['sampling','style','sort','yAxisOrientation','barChart','drawPoints','shaded','catmullRom']
+ this.options = util.selectiveBridgeObject(fields,options);
+ this.usingDefaultStyle = group.className === undefined;
+ this.groupsUsingDefaultStyles = groupsUsingDefaultStyles;
+ this.zeroPosition = 0;
+ this.update(group);
+ if (this.usingDefaultStyle == true) {
+ this.groupsUsingDefaultStyles[0] += 1;
}
+ this.itemsData = [];
+ this.visible = group.visible === undefined ? true : group.visible;
+ }
- return d;
- };
/**
- * This uses either the chordal or centripetal parameterization of the catmull-rom algorithm.
- * By default, the centripetal parameterization is used because this gives the nicest results.
- * These parameterizations are relatively heavy because the distance between 4 points have to be calculated.
- *
- * One optimization can be used to reuse distances since this is a sliding window approach.
- * @param data
- * @param group
- * @returns {string}
- * @private
+ * this loads a reference to all items in this group into this group.
+ * @param {array} items
*/
- Line._catmullRom = function(data, group) {
- var alpha = group.options.catmullRom.alpha;
- if (alpha == 0 || alpha === undefined) {
- return this._catmullRomUniform(data);
+ GraphGroup.prototype.setItems = function(items) {
+ if (items != null) {
+ this.itemsData = items;
+ if (this.options.sort == true) {
+ this.itemsData.sort(function (a,b) {return a.x - b.x;})
+ }
}
else {
- var p0, p1, p2, p3, bp1, bp2, d1,d2,d3, A, B, N, M;
- var d3powA, d2powA, d3pow2A, d2pow2A, d1pow2A, d1powA;
- var d = Math.round(data[0].x) + ',' + Math.round(data[0].y) + ' ';
- var length = data.length;
- for (var i = 0; i < length - 1; i++) {
+ this.itemsData = [];
+ }
+ };
- p0 = (i == 0) ? data[0] : data[i-1];
- p1 = data[i];
- p2 = data[i+1];
- p3 = (i + 2 < length) ? data[i+2] : p2;
- d1 = Math.sqrt(Math.pow(p0.x - p1.x,2) + Math.pow(p0.y - p1.y,2));
- d2 = Math.sqrt(Math.pow(p1.x - p2.x,2) + Math.pow(p1.y - p2.y,2));
- d3 = Math.sqrt(Math.pow(p2.x - p3.x,2) + Math.pow(p2.y - p3.y,2));
-
- // Catmull-Rom to Cubic Bezier conversion matrix
-
- // A = 2d1^2a + 3d1^a * d2^a + d3^2a
- // B = 2d3^2a + 3d3^a * d2^a + d2^2a
-
- // [ 0 1 0 0 ]
- // [ -d2^2a /N A/N d1^2a /N 0 ]
- // [ 0 d3^2a /M B/M -d2^2a /M ]
- // [ 0 0 1 0 ]
-
- d3powA = Math.pow(d3, alpha);
- d3pow2A = Math.pow(d3,2*alpha);
- d2powA = Math.pow(d2, alpha);
- d2pow2A = Math.pow(d2,2*alpha);
- d1powA = Math.pow(d1, alpha);
- d1pow2A = Math.pow(d1,2*alpha);
+ /**
+ * this is used for plotting barcharts, this way, we only have to calculate it once.
+ * @param pos
+ */
+ GraphGroup.prototype.setZeroPosition = function(pos) {
+ this.zeroPosition = pos;
+ };
- A = 2*d1pow2A + 3*d1powA * d2powA + d2pow2A;
- B = 2*d3pow2A + 3*d3powA * d2powA + d2pow2A;
- N = 3*d1powA * (d1powA + d2powA);
- if (N > 0) {N = 1 / N;}
- M = 3*d3powA * (d3powA + d2powA);
- if (M > 0) {M = 1 / M;}
- bp1 = { x: ((-d2pow2A * p0.x + A*p1.x + d1pow2A * p2.x) * N),
- y: ((-d2pow2A * p0.y + A*p1.y + d1pow2A * p2.y) * N)};
+ /**
+ * set the options of the graph group over the default options.
+ * @param options
+ */
+ GraphGroup.prototype.setOptions = function(options) {
+ if (options !== undefined) {
+ var fields = ['sampling','style','sort','yAxisOrientation','barChart'];
+ util.selectiveDeepExtend(fields, this.options, options);
- bp2 = { x: (( d3pow2A * p1.x + B*p2.x - d2pow2A * p3.x) * M),
- y: (( d3pow2A * p1.y + B*p2.y - d2pow2A * p3.y) * M)};
+ util.mergeOptions(this.options, options,'catmullRom');
+ util.mergeOptions(this.options, options,'drawPoints');
+ util.mergeOptions(this.options, options,'shaded');
- if (bp1.x == 0 && bp1.y == 0) {bp1 = p1;}
- if (bp2.x == 0 && bp2.y == 0) {bp2 = p2;}
- d += 'C' +
- bp1.x + ',' +
- bp1.y + ' ' +
- bp2.x + ',' +
- bp2.y + ' ' +
- p2.x + ',' +
- p2.y + ' ';
+ if (options.catmullRom) {
+ if (typeof options.catmullRom == 'object') {
+ if (options.catmullRom.parametrization) {
+ if (options.catmullRom.parametrization == 'uniform') {
+ this.options.catmullRom.alpha = 0;
+ }
+ else if (options.catmullRom.parametrization == 'chordal') {
+ this.options.catmullRom.alpha = 1.0;
+ }
+ else {
+ this.options.catmullRom.parametrization = 'centripetal';
+ this.options.catmullRom.alpha = 0.5;
+ }
+ }
+ }
}
+ }
- return d;
+ if (this.options.style == 'line') {
+ this.type = new Line(this.id, this.options);
+ }
+ else if (this.options.style == 'bar') {
+ this.type = new Bar(this.id, this.options);
+ }
+ else if (this.options.style == 'points') {
+ this.type = new Points(this.id, this.options);
}
};
+
/**
- * this generates the SVG path for a linear drawing between datapoints.
- * @param data
- * @returns {string}
- * @private
+ * this updates the current group class with the latest group dataset entree, used in _updateGroup in linegraph
+ * @param group
*/
- Line._linear = function(data) {
- // linear
- var d = '';
- for (var i = 0; i < data.length; i++) {
- if (i == 0) {
- d += data[i].x + ',' + data[i].y;
- }
- else {
- d += ' ' + data[i].x + ',' + data[i].y;
- }
- }
- return d;
+ GraphGroup.prototype.update = function(group) {
+ this.group = group;
+ this.content = group.content || 'graph';
+ this.className = group.className || this.className || "graphGroup" + this.groupsUsingDefaultStyles[0] % 10;
+ this.visible = group.visible === undefined ? true : group.visible;
+ this.style = group.style;
+ this.setOptions(group.options);
};
- module.exports = Line;
-
-
-/***/ },
-/* 48 */
-/***/ function(module, exports, __webpack_require__) {
/**
- * Created by Alex on 11/11/2014.
+ * draw the icon for the legend.
+ *
+ * @param x
+ * @param y
+ * @param JSONcontainer
+ * @param SVGcontainer
+ * @param iconWidth
+ * @param iconHeight
*/
- var DOMutil = __webpack_require__(6);
+ GraphGroup.prototype.drawIcon = function(x, y, JSONcontainer, SVGcontainer, iconWidth, iconHeight) {
+ var fillHeight = iconHeight * 0.5;
+ var path, fillPath;
- function Points(groupId, options) {
- this.groupId = groupId;
- this.options = options;
- }
+ var outline = DOMutil.getSVGElement("rect", JSONcontainer, SVGcontainer);
+ outline.setAttributeNS(null, "x", x);
+ outline.setAttributeNS(null, "y", y - fillHeight);
+ outline.setAttributeNS(null, "width", iconWidth);
+ outline.setAttributeNS(null, "height", 2*fillHeight);
+ outline.setAttributeNS(null, "class", "outline");
+ if (this.options.style == 'line') {
+ path = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer);
+ path.setAttributeNS(null, "class", this.className);
+ if(this.style !== undefined) {
+ path.setAttributeNS(null, "style", this.style);
+ }
- Points.prototype.getYRange = function(groupData) {
- var yMin = groupData[0].y;
- var yMax = groupData[0].y;
- for (var j = 0; j < groupData.length; j++) {
- yMin = yMin > groupData[j].y ? groupData[j].y : yMin;
- yMax = yMax < groupData[j].y ? groupData[j].y : yMax;
+ path.setAttributeNS(null, "d", "M" + x + ","+y+" L" + (x + iconWidth) + ","+y+"");
+ if (this.options.shaded.enabled == true) {
+ fillPath = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer);
+ if (this.options.shaded.orientation == 'top') {
+ fillPath.setAttributeNS(null, "d", "M"+x+", " + (y - fillHeight) +
+ "L"+x+","+y+" L"+ (x + iconWidth) + ","+y+" L"+ (x + iconWidth) + "," + (y - fillHeight));
+ }
+ else {
+ fillPath.setAttributeNS(null, "d", "M"+x+","+y+" " +
+ "L"+x+"," + (y + fillHeight) + " " +
+ "L"+ (x + iconWidth) + "," + (y + fillHeight) +
+ "L"+ (x + iconWidth) + ","+y);
+ }
+ fillPath.setAttributeNS(null, "class", this.className + " iconFill");
+ }
+
+ if (this.options.drawPoints.enabled == true) {
+ DOMutil.drawPoint(x + 0.5 * iconWidth,y, this, JSONcontainer, SVGcontainer);
+ }
+ }
+ else {
+ var barWidth = Math.round(0.3 * iconWidth);
+ var bar1Height = Math.round(0.4 * iconHeight);
+ var bar2Height = Math.round(0.75 * iconHeight);
+
+ var offset = Math.round((iconWidth - (2 * barWidth))/3);
+
+ DOMutil.drawBar(x + 0.5*barWidth + offset , y + fillHeight - bar1Height - 1, barWidth, bar1Height, this.className + ' bar', JSONcontainer, SVGcontainer);
+ DOMutil.drawBar(x + 1.5*barWidth + offset + 2, y + fillHeight - bar2Height - 1, barWidth, bar2Height, this.className + ' bar', JSONcontainer, SVGcontainer);
}
- return {min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation};
};
- Points.prototype.draw = function(dataset, group, framework, offset) {
- Points.draw(dataset, group, framework, offset);
- }
/**
- * draw the data points
+ * return the legend entree for this group.
*
- * @param {Array} dataset
- * @param {Object} JSONcontainer
- * @param {Object} svg | SVG DOM element
- * @param {GraphGroup} group
- * @param {Number} [offset]
+ * @param iconWidth
+ * @param iconHeight
+ * @returns {{icon: HTMLElement, label: (group.content|*|string), orientation: (.options.yAxisOrientation|*)}}
*/
- Points.draw = function (dataset, group, framework, offset) {
- if (offset === undefined) {offset = 0;}
- for (var i = 0; i < dataset.length; i++) {
- DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, group, framework.svgElements, framework.svg);
- }
- };
+ GraphGroup.prototype.getLegend = function(iconWidth, iconHeight) {
+ var svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
+ this.drawIcon(0,0.5*iconHeight,[],svg,iconWidth,iconHeight);
+ return {icon: svg, label: this.content, orientation:this.options.yAxisOrientation};
+ }
+ GraphGroup.prototype.getYRange = function(groupData) {
+ return this.type.getYRange(groupData);
+ }
+
+ GraphGroup.prototype.draw = function(dataset, group, framework) {
+ this.type.draw(dataset, group, framework);
+ }
+
+
+ module.exports = GraphGroup;
- module.exports = Points;
/***/ },
-/* 49 */
+/* 47 */
/***/ function(module, exports, __webpack_require__) {
/**
@@ -22051,496 +21779,768 @@ return /******/ (function(modules) { // webpackBootstrap
var DOMutil = __webpack_require__(6);
var Points = __webpack_require__(48);
- function Bargraph(groupId, options) {
+ function Line(groupId, options) {
this.groupId = groupId;
this.options = options;
}
- Bargraph.prototype.getYRange = function(groupData) {
- if (this.options.barChart.handleOverlap != 'stack') {
- var yMin = groupData[0].y;
- var yMax = groupData[0].y;
- for (var j = 0; j < groupData.length; j++) {
- yMin = yMin > groupData[j].y ? groupData[j].y : yMin;
- yMax = yMax < groupData[j].y ? groupData[j].y : yMax;
- }
- return {min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation};
- }
- else {
- var barCombinedData = [];
- for (var j = 0; j < groupData.length; j++) {
- barCombinedData.push({
- x: groupData[j].x,
- y: groupData[j].y,
- groupId: this.groupId
- });
- }
- return barCombinedData;
+ Line.prototype.getYRange = function(groupData) {
+ var yMin = groupData[0].y;
+ var yMax = groupData[0].y;
+ for (var j = 0; j < groupData.length; j++) {
+ yMin = yMin > groupData[j].y ? groupData[j].y : yMin;
+ yMax = yMax < groupData[j].y ? groupData[j].y : yMax;
}
+ return {min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation};
};
-
/**
- * draw a bar graph
+ * draw a line graph
*
- * @param groupIds
- * @param processedGroupData
+ * @param dataset
+ * @param group
*/
- Bargraph.draw = function (groupIds, processedGroupData, framework) {
- var combinedData = [];
- var intersections = {};
- var coreDistance;
- var key, drawData;
- var group;
- var i,j;
- var barPoints = 0;
-
- // combine all barchart data
- for (i = 0; i < groupIds.length; i++) {
- group = framework.groups[groupIds[i]];
- if (group.options.style == 'bar') {
- if (group.visible == true && (framework.options.groups.visibility[groupIds[i]] === undefined || framework.options.groups.visibility[groupIds[i]] == true)) {
- for (j = 0; j < processedGroupData[groupIds[i]].length; j++) {
- combinedData.push({
- x: processedGroupData[groupIds[i]][j].x,
- y: processedGroupData[groupIds[i]][j].y,
- groupId: groupIds[i]
- });
- barPoints += 1;
- }
+ Line.prototype.draw = function (dataset, group, framework) {
+ if (dataset != null) {
+ if (dataset.length > 0) {
+ var path, d;
+ var svgHeight = Number(framework.svg.style.height.replace('px',''));
+ path = DOMutil.getSVGElement('path', framework.svgElements, framework.svg);
+ path.setAttributeNS(null, "class", group.className);
+ if(group.style !== undefined) {
+ path.setAttributeNS(null, "style", group.style);
}
- }
- }
-
- if (barPoints == 0) {return;}
-
- // sort by time and by group
- combinedData.sort(function (a, b) {
- if (a.x == b.x) {
- return a.groupId - b.groupId;
- } else {
- return a.x - b.x;
- }
- });
-
- // get intersections
- Bargraph._getDataIntersections(intersections, combinedData);
-
- // plot barchart
- for (i = 0; i < combinedData.length; i++) {
- group = framework.groups[combinedData[i].groupId];
- var minWidth = 0.1 * group.options.barChart.width;
- key = combinedData[i].x;
- var heightOffset = 0;
- if (intersections[key] === undefined) {
- if (i+1 < combinedData.length) {coreDistance = Math.abs(combinedData[i+1].x - key);}
- if (i > 0) {coreDistance = Math.min(coreDistance,Math.abs(combinedData[i-1].x - key));}
- drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth);
- }
- else {
- var nextKey = i + (intersections[key].amount - intersections[key].resolved);
- var prevKey = i - (intersections[key].resolved + 1);
- if (nextKey < combinedData.length) {coreDistance = Math.abs(combinedData[nextKey].x - key);}
- if (prevKey > 0) {coreDistance = Math.min(coreDistance,Math.abs(combinedData[prevKey].x - key));}
- drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth);
- intersections[key].resolved += 1;
+ // construct path from dataset
+ if (group.options.catmullRom.enabled == true) {
+ d = Line._catmullRom(dataset, group);
+ }
+ else {
+ d = Line._linear(dataset);
+ }
- if (group.options.barChart.handleOverlap == 'stack') {
- heightOffset = intersections[key].accumulated;
- intersections[key].accumulated += group.zeroPosition - combinedData[i].y;
+ // append with points for fill and finalize the path
+ if (group.options.shaded.enabled == true) {
+ var fillPath = DOMutil.getSVGElement('path', framework.svgElements, framework.svg);
+ var dFill;
+ if (group.options.shaded.orientation == 'top') {
+ dFill = 'M' + dataset[0].x + ',' + 0 + ' ' + d + 'L' + dataset[dataset.length - 1].x + ',' + 0;
+ }
+ else {
+ dFill = 'M' + dataset[0].x + ',' + svgHeight + ' ' + d + 'L' + dataset[dataset.length - 1].x + ',' + svgHeight;
+ }
+ fillPath.setAttributeNS(null, "class", group.className + " fill");
+ if(group.options.shaded.style !== undefined) {
+ fillPath.setAttributeNS(null, "style", group.options.shaded.style);
+ }
+ fillPath.setAttributeNS(null, "d", dFill);
}
- else if (group.options.barChart.handleOverlap == 'sideBySide') {
- drawData.width = drawData.width / intersections[key].amount;
- drawData.offset += (intersections[key].resolved) * drawData.width - (0.5*drawData.width * (intersections[key].amount+1));
- if (group.options.barChart.align == 'left') {drawData.offset -= 0.5*drawData.width;}
- else if (group.options.barChart.align == 'right') {drawData.offset += 0.5*drawData.width;}
+ // copy properties to path for drawing.
+ path.setAttributeNS(null, 'd', 'M' + d);
+
+ // draw points
+ if (group.options.drawPoints.enabled == true) {
+ Points.draw(dataset, group, framework);
}
}
- DOMutil.drawBar(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, drawData.width, group.zeroPosition - combinedData[i].y, group.className + ' bar', framework.svgElements, framework.svg);
- // draw points
- if (group.options.drawPoints.enabled == true) {
- DOMutil.drawPoint(combinedData[i].x + drawData.offset, combinedData[i].y, group, framework.svgElements, framework.svg);
- }
}
};
+
/**
- * Fill the intersections object with counters of how many datapoints share the same x coordinates
- * @param intersections
- * @param combinedData
+ * This uses an uniform parametrization of the CatmullRom algorithm:
+ * 'On the Parameterization of Catmull-Rom Curves' by Cem Yuksel et al.
+ * @param data
+ * @returns {string}
* @private
*/
- Bargraph._getDataIntersections = function (intersections, combinedData) {
- // get intersections
- var coreDistance;
- for (var i = 0; i < combinedData.length; i++) {
- if (i + 1 < combinedData.length) {
- coreDistance = Math.abs(combinedData[i + 1].x - combinedData[i].x);
- }
- if (i > 0) {
- coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - combinedData[i].x));
- }
- if (coreDistance == 0) {
- if (intersections[combinedData[i].x] === undefined) {
- intersections[combinedData[i].x] = {amount: 0, resolved: 0, accumulated: 0};
- }
- intersections[combinedData[i].x].amount += 1;
- }
+ Line._catmullRomUniform = function(data) {
+ // catmull rom
+ var p0, p1, p2, p3, bp1, bp2;
+ var d = Math.round(data[0].x) + ',' + Math.round(data[0].y) + ' ';
+ var normalization = 1/6;
+ var length = data.length;
+ for (var i = 0; i < length - 1; i++) {
+
+ p0 = (i == 0) ? data[0] : data[i-1];
+ p1 = data[i];
+ p2 = data[i+1];
+ p3 = (i + 2 < length) ? data[i+2] : p2;
+
+
+ // Catmull-Rom to Cubic Bezier conversion matrix
+ // 0 1 0 0
+ // -1/6 1 1/6 0
+ // 0 1/6 1 -1/6
+ // 0 0 1 0
+
+ // bp0 = { x: p1.x, y: p1.y };
+ bp1 = { x: ((-p0.x + 6*p1.x + p2.x) *normalization), y: ((-p0.y + 6*p1.y + p2.y) *normalization)};
+ bp2 = { x: (( p1.x + 6*p2.x - p3.x) *normalization), y: (( p1.y + 6*p2.y - p3.y) *normalization)};
+ // bp0 = { x: p2.x, y: p2.y };
+
+ d += 'C' +
+ bp1.x + ',' +
+ bp1.y + ' ' +
+ bp2.x + ',' +
+ bp2.y + ' ' +
+ p2.x + ',' +
+ p2.y + ' ';
}
- };
+ return d;
+ };
/**
- * Get the width and offset for bargraphs based on the coredistance between datapoints
+ * This uses either the chordal or centripetal parameterization of the catmull-rom algorithm.
+ * By default, the centripetal parameterization is used because this gives the nicest results.
+ * These parameterizations are relatively heavy because the distance between 4 points have to be calculated.
*
- * @param coreDistance
+ * One optimization can be used to reuse distances since this is a sliding window approach.
+ * @param data
* @param group
- * @param minWidth
- * @returns {{width: Number, offset: Number}}
+ * @returns {string}
* @private
*/
- Bargraph._getSafeDrawData = function (coreDistance, group, minWidth) {
- var width, offset;
- if (coreDistance < group.options.barChart.width && coreDistance > 0) {
- width = coreDistance < minWidth ? minWidth : coreDistance;
-
- offset = 0; // recalculate offset with the new width;
- if (group.options.barChart.align == 'left') {
- offset -= 0.5 * coreDistance;
- }
- else if (group.options.barChart.align == 'right') {
- offset += 0.5 * coreDistance;
- }
+ Line._catmullRom = function(data, group) {
+ var alpha = group.options.catmullRom.alpha;
+ if (alpha == 0 || alpha === undefined) {
+ return this._catmullRomUniform(data);
}
else {
- // default settings
- width = group.options.barChart.width;
- offset = 0;
- if (group.options.barChart.align == 'left') {
- offset -= 0.5 * group.options.barChart.width;
- }
- else if (group.options.barChart.align == 'right') {
- offset += 0.5 * group.options.barChart.width;
- }
- }
+ var p0, p1, p2, p3, bp1, bp2, d1,d2,d3, A, B, N, M;
+ var d3powA, d2powA, d3pow2A, d2pow2A, d1pow2A, d1powA;
+ var d = Math.round(data[0].x) + ',' + Math.round(data[0].y) + ' ';
+ var length = data.length;
+ for (var i = 0; i < length - 1; i++) {
- return {width: width, offset: offset};
- };
+ p0 = (i == 0) ? data[0] : data[i-1];
+ p1 = data[i];
+ p2 = data[i+1];
+ p3 = (i + 2 < length) ? data[i+2] : p2;
- Bargraph.getStackedBarYRange = function(barCombinedData, groupRanges, groupIds, groupLabel, orientation) {
- if (barCombinedData.length > 0) {
- // sort by time and by group
- barCombinedData.sort(function (a, b) {
- if (a.x == b.x) {
- return a.groupId - b.groupId;
- } else {
- return a.x - b.x;
- }
- });
- var intersections = {};
+ d1 = Math.sqrt(Math.pow(p0.x - p1.x,2) + Math.pow(p0.y - p1.y,2));
+ d2 = Math.sqrt(Math.pow(p1.x - p2.x,2) + Math.pow(p1.y - p2.y,2));
+ d3 = Math.sqrt(Math.pow(p2.x - p3.x,2) + Math.pow(p2.y - p3.y,2));
- Bargraph._getDataIntersections(intersections, barCombinedData);
- groupRanges[groupLabel] = Bargraph._getStackedBarYRange(intersections, barCombinedData);
- groupRanges[groupLabel].yAxisOrientation = orientation;
- groupIds.push(groupLabel);
- }
- }
+ // Catmull-Rom to Cubic Bezier conversion matrix
- Bargraph._getStackedBarYRange = function (intersections, combinedData) {
- var key;
- var yMin = combinedData[0].y;
- var yMax = combinedData[0].y;
- for (var i = 0; i < combinedData.length; i++) {
- key = combinedData[i].x;
- if (intersections[key] === undefined) {
- yMin = yMin > combinedData[i].y ? combinedData[i].y : yMin;
- yMax = yMax < combinedData[i].y ? combinedData[i].y : yMax;
- }
- else {
- intersections[key].accumulated += combinedData[i].y;
- }
- }
- for (var xpos in intersections) {
- if (intersections.hasOwnProperty(xpos)) {
- yMin = yMin > intersections[xpos].accumulated ? intersections[xpos].accumulated : yMin;
- yMax = yMax < intersections[xpos].accumulated ? intersections[xpos].accumulated : yMax;
- }
- }
+ // A = 2d1^2a + 3d1^a * d2^a + d3^2a
+ // B = 2d3^2a + 3d3^a * d2^a + d2^2a
- return {min: yMin, max: yMax};
- };
+ // [ 0 1 0 0 ]
+ // [ -d2^2a /N A/N d1^2a /N 0 ]
+ // [ 0 d3^2a /M B/M -d2^2a /M ]
+ // [ 0 0 1 0 ]
- module.exports = Bargraph;
+ d3powA = Math.pow(d3, alpha);
+ d3pow2A = Math.pow(d3,2*alpha);
+ d2powA = Math.pow(d2, alpha);
+ d2pow2A = Math.pow(d2,2*alpha);
+ d1powA = Math.pow(d1, alpha);
+ d1pow2A = Math.pow(d1,2*alpha);
-/***/ },
-/* 50 */
-/***/ function(module, exports, __webpack_require__) {
+ A = 2*d1pow2A + 3*d1powA * d2powA + d2pow2A;
+ B = 2*d3pow2A + 3*d3powA * d2powA + d2pow2A;
+ N = 3*d1powA * (d1powA + d2powA);
+ if (N > 0) {N = 1 / N;}
+ M = 3*d3powA * (d3powA + d2powA);
+ if (M > 0) {M = 1 / M;}
- var util = __webpack_require__(1);
- var DOMutil = __webpack_require__(6);
- var Component = __webpack_require__(23);
+ bp1 = { x: ((-d2pow2A * p0.x + A*p1.x + d1pow2A * p2.x) * N),
+ y: ((-d2pow2A * p0.y + A*p1.y + d1pow2A * p2.y) * N)};
+
+ bp2 = { x: (( d3pow2A * p1.x + B*p2.x - d2pow2A * p3.x) * M),
+ y: (( d3pow2A * p1.y + B*p2.y - d2pow2A * p3.y) * M)};
+
+ if (bp1.x == 0 && bp1.y == 0) {bp1 = p1;}
+ if (bp2.x == 0 && bp2.y == 0) {bp2 = p2;}
+ d += 'C' +
+ bp1.x + ',' +
+ bp1.y + ' ' +
+ bp2.x + ',' +
+ bp2.y + ' ' +
+ p2.x + ',' +
+ p2.y + ' ';
+ }
+
+ return d;
+ }
+ };
/**
- * Legend for Graph2d
+ * this generates the SVG path for a linear drawing between datapoints.
+ * @param data
+ * @returns {string}
+ * @private
*/
- function Legend(body, options, side, linegraphOptions) {
- this.body = body;
- this.defaultOptions = {
- enabled: true,
- icons: true,
- iconSize: 20,
- iconSpacing: 6,
- left: {
- visible: true,
- position: 'top-left' // top/bottom - left,center,right
- },
- right: {
- visible: true,
- position: 'top-left' // top/bottom - left,center,right
+ Line._linear = function(data) {
+ // linear
+ var d = '';
+ for (var i = 0; i < data.length; i++) {
+ if (i == 0) {
+ d += data[i].x + ',' + data[i].y;
+ }
+ else {
+ d += ' ' + data[i].x + ',' + data[i].y;
}
}
- this.side = side;
- this.options = util.extend({},this.defaultOptions);
- this.linegraphOptions = linegraphOptions;
+ return d;
+ };
- this.svgElements = {};
- this.dom = {};
- this.groups = {};
- this.amountOfGroups = 0;
- this._create();
+ module.exports = Line;
- this.setOptions(options);
- }
- Legend.prototype = new Component();
+/***/ },
+/* 48 */
+/***/ function(module, exports, __webpack_require__) {
- Legend.prototype.clear = function() {
- this.groups = {};
- this.amountOfGroups = 0;
+ /**
+ * Created by Alex on 11/11/2014.
+ */
+ var DOMutil = __webpack_require__(6);
+
+ function Points(groupId, options) {
+ this.groupId = groupId;
+ this.options = options;
}
- Legend.prototype.addGroup = function(label, graphOptions) {
- if (!this.groups.hasOwnProperty(label)) {
- this.groups[label] = graphOptions;
+ Points.prototype.getYRange = function(groupData) {
+ var yMin = groupData[0].y;
+ var yMax = groupData[0].y;
+ for (var j = 0; j < groupData.length; j++) {
+ yMin = yMin > groupData[j].y ? groupData[j].y : yMin;
+ yMax = yMax < groupData[j].y ? groupData[j].y : yMax;
}
- this.amountOfGroups += 1;
+ return {min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation};
};
- Legend.prototype.updateGroup = function(label, graphOptions) {
- this.groups[label] = graphOptions;
- };
+ Points.prototype.draw = function(dataset, group, framework, offset) {
+ Points.draw(dataset, group, framework, offset);
+ }
- Legend.prototype.removeGroup = function(label) {
- if (this.groups.hasOwnProperty(label)) {
- delete this.groups[label];
- this.amountOfGroups -= 1;
+ /**
+ * draw the data points
+ *
+ * @param {Array} dataset
+ * @param {Object} JSONcontainer
+ * @param {Object} svg | SVG DOM element
+ * @param {GraphGroup} group
+ * @param {Number} [offset]
+ */
+ Points.draw = function (dataset, group, framework, offset) {
+ if (offset === undefined) {offset = 0;}
+ for (var i = 0; i < dataset.length; i++) {
+ DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, group, framework.svgElements, framework.svg);
}
};
- Legend.prototype._create = function() {
- this.dom.frame = document.createElement('div');
- this.dom.frame.className = 'legend';
- this.dom.frame.style.position = "absolute";
- this.dom.frame.style.top = "10px";
- this.dom.frame.style.display = "block";
-
- this.dom.textArea = document.createElement('div');
- this.dom.textArea.className = 'legendText';
- this.dom.textArea.style.position = "relative";
- this.dom.textArea.style.top = "0px";
- this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
- this.svg.style.position = 'absolute';
- this.svg.style.top = 0 +'px';
- this.svg.style.width = this.options.iconSize + 5 + 'px';
- this.svg.style.height = '100%';
+ module.exports = Points;
- this.dom.frame.appendChild(this.svg);
- this.dom.frame.appendChild(this.dom.textArea);
- };
+/***/ },
+/* 49 */
+/***/ function(module, exports, __webpack_require__) {
/**
- * Hide the component from the DOM
+ * Created by Alex on 11/11/2014.
*/
- Legend.prototype.hide = function() {
- // remove the frame containing the items
- if (this.dom.frame.parentNode) {
- this.dom.frame.parentNode.removeChild(this.dom.frame);
+ var DOMutil = __webpack_require__(6);
+ var Points = __webpack_require__(48);
+
+ function Bargraph(groupId, options) {
+ this.groupId = groupId;
+ this.options = options;
+ }
+
+ Bargraph.prototype.getYRange = function(groupData) {
+ if (this.options.barChart.handleOverlap != 'stack') {
+ var yMin = groupData[0].y;
+ var yMax = groupData[0].y;
+ for (var j = 0; j < groupData.length; j++) {
+ yMin = yMin > groupData[j].y ? groupData[j].y : yMin;
+ yMax = yMax < groupData[j].y ? groupData[j].y : yMax;
+ }
+ return {min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation};
+ }
+ else {
+ var barCombinedData = [];
+ for (var j = 0; j < groupData.length; j++) {
+ barCombinedData.push({
+ x: groupData[j].x,
+ y: groupData[j].y,
+ groupId: this.groupId
+ });
+ }
+ return barCombinedData;
}
};
+
+
/**
- * Show the component in the DOM (when not already visible).
- * @return {Boolean} changed
+ * draw a bar graph
+ *
+ * @param groupIds
+ * @param processedGroupData
*/
- Legend.prototype.show = function() {
- // show frame containing the items
- if (!this.dom.frame.parentNode) {
- this.body.dom.center.appendChild(this.dom.frame);
- }
- };
-
- Legend.prototype.setOptions = function(options) {
- var fields = ['enabled','orientation','icons','left','right'];
- util.selectiveDeepExtend(fields, this.options, options);
- };
+ Bargraph.draw = function (groupIds, processedGroupData, framework) {
+ var combinedData = [];
+ var intersections = {};
+ var coreDistance;
+ var key, drawData;
+ var group;
+ var i,j;
+ var barPoints = 0;
- Legend.prototype.redraw = function() {
- var activeGroups = 0;
- for (var groupId in this.groups) {
- if (this.groups.hasOwnProperty(groupId)) {
- if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
- activeGroups++;
+ // combine all barchart data
+ for (i = 0; i < groupIds.length; i++) {
+ group = framework.groups[groupIds[i]];
+ if (group.options.style == 'bar') {
+ if (group.visible == true && (framework.options.groups.visibility[groupIds[i]] === undefined || framework.options.groups.visibility[groupIds[i]] == true)) {
+ for (j = 0; j < processedGroupData[groupIds[i]].length; j++) {
+ combinedData.push({
+ x: processedGroupData[groupIds[i]][j].x,
+ y: processedGroupData[groupIds[i]][j].y,
+ groupId: groupIds[i]
+ });
+ barPoints += 1;
+ }
}
}
}
- if (this.options[this.side].visible == false || this.amountOfGroups == 0 || this.options.enabled == false || activeGroups == 0) {
- this.hide();
- }
- else {
- this.show();
- if (this.options[this.side].position == 'top-left' || this.options[this.side].position == 'bottom-left') {
- this.dom.frame.style.left = '4px';
- this.dom.frame.style.textAlign = "left";
- this.dom.textArea.style.textAlign = "left";
- this.dom.textArea.style.left = (this.options.iconSize + 15) + 'px';
- this.dom.textArea.style.right = '';
- this.svg.style.left = 0 +'px';
- this.svg.style.right = '';
- }
- else {
- this.dom.frame.style.right = '4px';
- this.dom.frame.style.textAlign = "right";
- this.dom.textArea.style.textAlign = "right";
- this.dom.textArea.style.right = (this.options.iconSize + 15) + 'px';
- this.dom.textArea.style.left = '';
- this.svg.style.right = 0 +'px';
- this.svg.style.left = '';
- }
+ if (barPoints == 0) {return;}
- if (this.options[this.side].position == 'top-left' || this.options[this.side].position == 'top-right') {
- this.dom.frame.style.top = 4 - Number(this.body.dom.center.style.top.replace("px","")) + 'px';
- this.dom.frame.style.bottom = '';
- }
- else {
- var scrollableHeight = this.body.domProps.center.height - this.body.domProps.centerContainer.height;
- this.dom.frame.style.bottom = 4 + scrollableHeight + Number(this.body.dom.center.style.top.replace("px","")) + 'px';
- this.dom.frame.style.top = '';
+ // sort by time and by group
+ combinedData.sort(function (a, b) {
+ if (a.x == b.x) {
+ return a.groupId - b.groupId;
+ } else {
+ return a.x - b.x;
}
+ });
- if (this.options.icons == false) {
- this.dom.frame.style.width = this.dom.textArea.offsetWidth + 10 + 'px';
- this.dom.textArea.style.right = '';
- this.dom.textArea.style.left = '';
- this.svg.style.width = '0px';
+ // get intersections
+ Bargraph._getDataIntersections(intersections, combinedData);
+
+ // plot barchart
+ for (i = 0; i < combinedData.length; i++) {
+ group = framework.groups[combinedData[i].groupId];
+ var minWidth = 0.1 * group.options.barChart.width;
+
+ key = combinedData[i].x;
+ var heightOffset = 0;
+ if (intersections[key] === undefined) {
+ if (i+1 < combinedData.length) {coreDistance = Math.abs(combinedData[i+1].x - key);}
+ if (i > 0) {coreDistance = Math.min(coreDistance,Math.abs(combinedData[i-1].x - key));}
+ drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth);
}
else {
- this.dom.frame.style.width = this.options.iconSize + 15 + this.dom.textArea.offsetWidth + 10 + 'px'
- this.drawLegendIcons();
- }
+ var nextKey = i + (intersections[key].amount - intersections[key].resolved);
+ var prevKey = i - (intersections[key].resolved + 1);
+ if (nextKey < combinedData.length) {coreDistance = Math.abs(combinedData[nextKey].x - key);}
+ if (prevKey > 0) {coreDistance = Math.min(coreDistance,Math.abs(combinedData[prevKey].x - key));}
+ drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth);
+ intersections[key].resolved += 1;
- var content = '';
- for (var groupId in this.groups) {
- if (this.groups.hasOwnProperty(groupId)) {
- if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
- content += this.groups[groupId].content + '
';
- }
+ if (group.options.barChart.handleOverlap == 'stack') {
+ heightOffset = intersections[key].accumulated;
+ intersections[key].accumulated += group.zeroPosition - combinedData[i].y;
+ }
+ else if (group.options.barChart.handleOverlap == 'sideBySide') {
+ drawData.width = drawData.width / intersections[key].amount;
+ drawData.offset += (intersections[key].resolved) * drawData.width - (0.5*drawData.width * (intersections[key].amount+1));
+ if (group.options.barChart.align == 'left') {drawData.offset -= 0.5*drawData.width;}
+ else if (group.options.barChart.align == 'right') {drawData.offset += 0.5*drawData.width;}
}
}
- this.dom.textArea.innerHTML = content;
- this.dom.textArea.style.lineHeight = ((0.75 * this.options.iconSize) + this.options.iconSpacing) + 'px';
+ DOMutil.drawBar(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, drawData.width, group.zeroPosition - combinedData[i].y, group.className + ' bar', framework.svgElements, framework.svg);
+ // draw points
+ if (group.options.drawPoints.enabled == true) {
+ DOMutil.drawPoint(combinedData[i].x + drawData.offset, combinedData[i].y, group, framework.svgElements, framework.svg);
+ }
}
};
- Legend.prototype.drawLegendIcons = function() {
- if (this.dom.frame.parentNode) {
- DOMutil.prepareElements(this.svgElements);
- var padding = window.getComputedStyle(this.dom.frame).paddingTop;
- var iconOffset = Number(padding.replace('px',''));
- var x = iconOffset;
- var iconWidth = this.options.iconSize;
- var iconHeight = 0.75 * this.options.iconSize;
- var y = iconOffset + 0.5 * iconHeight + 3;
-
- this.svg.style.width = iconWidth + 5 + iconOffset + 'px';
- for (var groupId in this.groups) {
- if (this.groups.hasOwnProperty(groupId)) {
- if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
- this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight);
- y += iconHeight + this.options.iconSpacing;
- }
+ /**
+ * Fill the intersections object with counters of how many datapoints share the same x coordinates
+ * @param intersections
+ * @param combinedData
+ * @private
+ */
+ Bargraph._getDataIntersections = function (intersections, combinedData) {
+ // get intersections
+ var coreDistance;
+ for (var i = 0; i < combinedData.length; i++) {
+ if (i + 1 < combinedData.length) {
+ coreDistance = Math.abs(combinedData[i + 1].x - combinedData[i].x);
+ }
+ if (i > 0) {
+ coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - combinedData[i].x));
+ }
+ if (coreDistance == 0) {
+ if (intersections[combinedData[i].x] === undefined) {
+ intersections[combinedData[i].x] = {amount: 0, resolved: 0, accumulated: 0};
}
+ intersections[combinedData[i].x].amount += 1;
}
+ }
+ };
- DOMutil.cleanupElements(this.svgElements);
+
+ /**
+ * Get the width and offset for bargraphs based on the coredistance between datapoints
+ *
+ * @param coreDistance
+ * @param group
+ * @param minWidth
+ * @returns {{width: Number, offset: Number}}
+ * @private
+ */
+ Bargraph._getSafeDrawData = function (coreDistance, group, minWidth) {
+ var width, offset;
+ if (coreDistance < group.options.barChart.width && coreDistance > 0) {
+ width = coreDistance < minWidth ? minWidth : coreDistance;
+
+ offset = 0; // recalculate offset with the new width;
+ if (group.options.barChart.align == 'left') {
+ offset -= 0.5 * coreDistance;
+ }
+ else if (group.options.barChart.align == 'right') {
+ offset += 0.5 * coreDistance;
+ }
+ }
+ else {
+ // default settings
+ width = group.options.barChart.width;
+ offset = 0;
+ if (group.options.barChart.align == 'left') {
+ offset -= 0.5 * group.options.barChart.width;
+ }
+ else if (group.options.barChart.align == 'right') {
+ offset += 0.5 * group.options.barChart.width;
+ }
}
+
+ return {width: width, offset: offset};
};
- module.exports = Legend;
+ Bargraph.getStackedBarYRange = function(barCombinedData, groupRanges, groupIds, groupLabel, orientation) {
+ if (barCombinedData.length > 0) {
+ // sort by time and by group
+ barCombinedData.sort(function (a, b) {
+ if (a.x == b.x) {
+ return a.groupId - b.groupId;
+ } else {
+ return a.x - b.x;
+ }
+ });
+ var intersections = {};
+
+ Bargraph._getDataIntersections(intersections, barCombinedData);
+ groupRanges[groupLabel] = Bargraph._getStackedBarYRange(intersections, barCombinedData);
+ groupRanges[groupLabel].yAxisOrientation = orientation;
+ groupIds.push(groupLabel);
+ }
+ }
+
+ Bargraph._getStackedBarYRange = function (intersections, combinedData) {
+ var key;
+ var yMin = combinedData[0].y;
+ var yMax = combinedData[0].y;
+ for (var i = 0; i < combinedData.length; i++) {
+ key = combinedData[i].x;
+ if (intersections[key] === undefined) {
+ yMin = yMin > combinedData[i].y ? combinedData[i].y : yMin;
+ yMax = yMax < combinedData[i].y ? combinedData[i].y : yMax;
+ }
+ else {
+ intersections[key].accumulated += combinedData[i].y;
+ }
+ }
+ for (var xpos in intersections) {
+ if (intersections.hasOwnProperty(xpos)) {
+ yMin = yMin > intersections[xpos].accumulated ? intersections[xpos].accumulated : yMin;
+ yMax = yMax < intersections[xpos].accumulated ? intersections[xpos].accumulated : yMax;
+ }
+ }
+ return {min: yMin, max: yMax};
+ };
+
+ module.exports = Bargraph;
/***/ },
-/* 51 */
+/* 50 */
/***/ function(module, exports, __webpack_require__) {
- var Emitter = __webpack_require__(11);
- var Hammer = __webpack_require__(19);
- var keycharm = __webpack_require__(36);
var util = __webpack_require__(1);
- var hammerUtil = __webpack_require__(22);
- var DataSet = __webpack_require__(7);
- var DataView = __webpack_require__(9);
- var dotparser = __webpack_require__(52);
- var gephiParser = __webpack_require__(53);
- var Groups = __webpack_require__(54);
- var Images = __webpack_require__(55);
- var Node = __webpack_require__(56);
- var Edge = __webpack_require__(57);
- var Popup = __webpack_require__(58);
- var MixinLoader = __webpack_require__(59);
- var Activator = __webpack_require__(35);
- var locales = __webpack_require__(70);
-
- // Load custom shapes into CanvasRenderingContext2D
- __webpack_require__(71);
+ var DOMutil = __webpack_require__(6);
+ var Component = __webpack_require__(23);
/**
- * @constructor Network
- * Create a network visualization, displaying nodes and edges.
- *
- * @param {Element} container The DOM element in which the Network will
- * be created. Normally a div element.
- * @param {Object} data An object containing parameters
- * {Array} nodes
- * {Array} edges
- * @param {Object} options Options
+ * Legend for Graph2d
*/
- function Network (container, data, options) {
- if (!(this instanceof Network)) {
- throw new SyntaxError('Constructor must be called with the new operator');
+ function Legend(body, options, side, linegraphOptions) {
+ this.body = body;
+ this.defaultOptions = {
+ enabled: true,
+ icons: true,
+ iconSize: 20,
+ iconSpacing: 6,
+ left: {
+ visible: true,
+ position: 'top-left' // top/bottom - left,center,right
+ },
+ right: {
+ visible: true,
+ position: 'top-left' // top/bottom - left,center,right
+ }
}
+ this.side = side;
+ this.options = util.extend({},this.defaultOptions);
+ this.linegraphOptions = linegraphOptions;
- this._initializeMixinLoaders();
+ this.svgElements = {};
+ this.dom = {};
+ this.groups = {};
+ this.amountOfGroups = 0;
+ this._create();
- // create variables and set default values
- this.containerElement = container;
+ this.setOptions(options);
+ }
- // render and calculation settings
- this.renderRefreshRate = 60; // hz (fps)
- this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on
- this.renderTime = 0.5 * this.renderTimestep; // measured time it takes to render a frame
- this.maxPhysicsTicksPerRender = 3; // max amount of physics ticks per render step.
- this.physicsDiscreteStepsize = 0.50; // discrete stepsize of the simulation
+ Legend.prototype = new Component();
- this.initializing = true;
+ Legend.prototype.clear = function() {
+ this.groups = {};
+ this.amountOfGroups = 0;
+ }
- this.triggerFunctions = {add:null,edit:null,editEdge:null,connect:null,del:null};
+ Legend.prototype.addGroup = function(label, graphOptions) {
+
+ if (!this.groups.hasOwnProperty(label)) {
+ this.groups[label] = graphOptions;
+ }
+ this.amountOfGroups += 1;
+ };
+
+ Legend.prototype.updateGroup = function(label, graphOptions) {
+ this.groups[label] = graphOptions;
+ };
+
+ Legend.prototype.removeGroup = function(label) {
+ if (this.groups.hasOwnProperty(label)) {
+ delete this.groups[label];
+ this.amountOfGroups -= 1;
+ }
+ };
+
+ Legend.prototype._create = function() {
+ this.dom.frame = document.createElement('div');
+ this.dom.frame.className = 'legend';
+ this.dom.frame.style.position = "absolute";
+ this.dom.frame.style.top = "10px";
+ this.dom.frame.style.display = "block";
+
+ this.dom.textArea = document.createElement('div');
+ this.dom.textArea.className = 'legendText';
+ this.dom.textArea.style.position = "relative";
+ this.dom.textArea.style.top = "0px";
+
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
+ this.svg.style.position = 'absolute';
+ this.svg.style.top = 0 +'px';
+ this.svg.style.width = this.options.iconSize + 5 + 'px';
+ this.svg.style.height = '100%';
+
+ this.dom.frame.appendChild(this.svg);
+ this.dom.frame.appendChild(this.dom.textArea);
+ };
+
+ /**
+ * Hide the component from the DOM
+ */
+ Legend.prototype.hide = function() {
+ // remove the frame containing the items
+ if (this.dom.frame.parentNode) {
+ this.dom.frame.parentNode.removeChild(this.dom.frame);
+ }
+ };
+
+ /**
+ * Show the component in the DOM (when not already visible).
+ * @return {Boolean} changed
+ */
+ Legend.prototype.show = function() {
+ // show frame containing the items
+ if (!this.dom.frame.parentNode) {
+ this.body.dom.center.appendChild(this.dom.frame);
+ }
+ };
+
+ Legend.prototype.setOptions = function(options) {
+ var fields = ['enabled','orientation','icons','left','right'];
+ util.selectiveDeepExtend(fields, this.options, options);
+ };
+
+ Legend.prototype.redraw = function() {
+ var activeGroups = 0;
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
+ activeGroups++;
+ }
+ }
+ }
+
+ if (this.options[this.side].visible == false || this.amountOfGroups == 0 || this.options.enabled == false || activeGroups == 0) {
+ this.hide();
+ }
+ else {
+ this.show();
+ if (this.options[this.side].position == 'top-left' || this.options[this.side].position == 'bottom-left') {
+ this.dom.frame.style.left = '4px';
+ this.dom.frame.style.textAlign = "left";
+ this.dom.textArea.style.textAlign = "left";
+ this.dom.textArea.style.left = (this.options.iconSize + 15) + 'px';
+ this.dom.textArea.style.right = '';
+ this.svg.style.left = 0 +'px';
+ this.svg.style.right = '';
+ }
+ else {
+ this.dom.frame.style.right = '4px';
+ this.dom.frame.style.textAlign = "right";
+ this.dom.textArea.style.textAlign = "right";
+ this.dom.textArea.style.right = (this.options.iconSize + 15) + 'px';
+ this.dom.textArea.style.left = '';
+ this.svg.style.right = 0 +'px';
+ this.svg.style.left = '';
+ }
+
+ if (this.options[this.side].position == 'top-left' || this.options[this.side].position == 'top-right') {
+ this.dom.frame.style.top = 4 - Number(this.body.dom.center.style.top.replace("px","")) + 'px';
+ this.dom.frame.style.bottom = '';
+ }
+ else {
+ var scrollableHeight = this.body.domProps.center.height - this.body.domProps.centerContainer.height;
+ this.dom.frame.style.bottom = 4 + scrollableHeight + Number(this.body.dom.center.style.top.replace("px","")) + 'px';
+ this.dom.frame.style.top = '';
+ }
+
+ if (this.options.icons == false) {
+ this.dom.frame.style.width = this.dom.textArea.offsetWidth + 10 + 'px';
+ this.dom.textArea.style.right = '';
+ this.dom.textArea.style.left = '';
+ this.svg.style.width = '0px';
+ }
+ else {
+ this.dom.frame.style.width = this.options.iconSize + 15 + this.dom.textArea.offsetWidth + 10 + 'px'
+ this.drawLegendIcons();
+ }
+
+ var content = '';
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
+ content += this.groups[groupId].content + '
';
+ }
+ }
+ }
+ this.dom.textArea.innerHTML = content;
+ this.dom.textArea.style.lineHeight = ((0.75 * this.options.iconSize) + this.options.iconSpacing) + 'px';
+ }
+ };
+
+ Legend.prototype.drawLegendIcons = function() {
+ if (this.dom.frame.parentNode) {
+ DOMutil.prepareElements(this.svgElements);
+ var padding = window.getComputedStyle(this.dom.frame).paddingTop;
+ var iconOffset = Number(padding.replace('px',''));
+ var x = iconOffset;
+ var iconWidth = this.options.iconSize;
+ var iconHeight = 0.75 * this.options.iconSize;
+ var y = iconOffset + 0.5 * iconHeight + 3;
+
+ this.svg.style.width = iconWidth + 5 + iconOffset + 'px';
+
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
+ this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight);
+ y += iconHeight + this.options.iconSpacing;
+ }
+ }
+ }
+
+ DOMutil.cleanupElements(this.svgElements);
+ }
+ };
+
+ module.exports = Legend;
+
+
+/***/ },
+/* 51 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var Emitter = __webpack_require__(11);
+ var Hammer = __webpack_require__(19);
+ var keycharm = __webpack_require__(36);
+ var util = __webpack_require__(1);
+ var hammerUtil = __webpack_require__(22);
+ var DataSet = __webpack_require__(7);
+ var DataView = __webpack_require__(9);
+ var dotparser = __webpack_require__(57);
+ var gephiParser = __webpack_require__(58);
+ var Groups = __webpack_require__(54);
+ var Images = __webpack_require__(55);
+ var Node = __webpack_require__(53);
+ var Edge = __webpack_require__(52);
+ var Popup = __webpack_require__(56);
+ var MixinLoader = __webpack_require__(59);
+ var Activator = __webpack_require__(35);
+ var locales = __webpack_require__(70);
+
+ // Load custom shapes into CanvasRenderingContext2D
+ __webpack_require__(71);
+
+ /**
+ * @constructor Network
+ * Create a network visualization, displaying nodes and edges.
+ *
+ * @param {Element} container The DOM element in which the Network will
+ * be created. Normally a div element.
+ * @param {Object} data An object containing parameters
+ * {Array} nodes
+ * {Array} edges
+ * @param {Object} options Options
+ */
+ function Network (container, data, options) {
+ if (!(this instanceof Network)) {
+ throw new SyntaxError('Constructor must be called with the new operator');
+ }
+
+ this._initializeMixinLoaders();
+
+ // create variables and set default values
+ this.containerElement = container;
+
+ // render and calculation settings
+ this.renderRefreshRate = 60; // hz (fps)
+ this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on
+ this.renderTime = 0.5 * this.renderTimestep; // measured time it takes to render a frame
+ this.maxPhysicsTicksPerRender = 3; // max amount of physics ticks per render step.
+ this.physicsDiscreteStepsize = 0.50; // discrete stepsize of the simulation
+
+ this.initializing = true;
+
+ this.triggerFunctions = {add:null,edit:null,editEdge:null,connect:null,del:null};
// set constant values
this.defaultOptions = {
@@ -23792,6 +23792,7 @@ return /******/ (function(modules) { // webpackBootstrap
var id;
var lastPopupNode = this.popupObj;
+ var nodeUnderCursor = false;
if (this.popupObj == undefined) {
// search the nodes for overlap, select the top one in case of multiple nodes
@@ -23799,15 +23800,19 @@ return /******/ (function(modules) { // webpackBootstrap
for (id in nodes) {
if (nodes.hasOwnProperty(id)) {
var node = nodes[id];
- if (node.getTitle() !== undefined && node.isOverlappingWith(obj)) {
- this.popupObj = node;
- break;
+ if (node.isOverlappingWith(obj)) {
+ if (node.getTitle() !== undefined) {
+ this.popupObj = node;
+ break;
+ }
+ // if you hover over a node, the title of the edge is not supposed to be shown.
+ nodeUnderCursor = true;
}
}
}
}
- if (this.popupObj === undefined) {
+ if (this.popupObj === undefined && nodeUnderCursor == false) {
// search the edges for overlap
var edges = this.edges;
for (id in edges) {
@@ -25132,1045 +25137,1219 @@ return /******/ (function(modules) { // webpackBootstrap
/* 52 */
/***/ function(module, exports, __webpack_require__) {
+ var util = __webpack_require__(1);
+ var Node = __webpack_require__(53);
+
/**
- * Parse a text source containing data in DOT language into a JSON object.
- * The object contains two lists: one with nodes and one with edges.
- *
- * DOT language reference: http://www.graphviz.org/doc/info/lang.html
+ * @class Edge
*
- * @param {String} data Text containing a graph in DOT-notation
- * @return {Object} graph An object containing two parameters:
- * {Object[]} nodes
- * {Object[]} edges
+ * A edge connects two nodes
+ * @param {Object} properties Object with properties. Must contain
+ * At least properties from and to.
+ * Available properties: from (number),
+ * to (number), label (string, color (string),
+ * width (number), style (string),
+ * length (number), title (string)
+ * @param {Network} network A Network object, used to find and edge to
+ * nodes.
+ * @param {Object} constants An object with default values for
+ * example for the color
*/
- function parseDOT (data) {
- dot = data;
- return parseGraph();
- }
+ function Edge (properties, network, networkConstants) {
+ if (!network) {
+ throw "No network provided";
+ }
+ var fields = ['edges','physics'];
+ var constants = util.selectiveBridgeObject(fields,networkConstants);
+ this.options = constants.edges;
+ this.physics = constants.physics;
+ this.options['smoothCurves'] = networkConstants['smoothCurves'];
- // token types enumeration
- var TOKENTYPE = {
- NULL : 0,
- DELIMITER : 1,
- IDENTIFIER: 2,
- UNKNOWN : 3
- };
- // map with all delimiters
- var DELIMITERS = {
- '{': true,
- '}': true,
- '[': true,
- ']': true,
- ';': true,
- '=': true,
- ',': true,
+ this.network = network;
- '->': true,
- '--': true
- };
+ // initialize variables
+ this.id = undefined;
+ this.fromId = undefined;
+ this.toId = undefined;
+ this.title = undefined;
+ this.widthSelected = this.options.width * this.options.widthSelectionMultiplier;
+ this.value = undefined;
+ this.selected = false;
+ this.hover = false;
+ this.labelDimensions = {top:0,left:0,width:0,height:0,yLine:0}; // could be cached
+ this.dirtyLabel = true;
- var dot = ''; // current dot file
- var index = 0; // current index in dot file
- var c = ''; // current token character in expr
- var token = ''; // current token
- var tokenType = TOKENTYPE.NULL; // type of the token
+ this.from = null; // a node
+ this.to = null; // a node
+ this.via = null; // a temp node
- /**
- * Get the first character from the dot file.
- * The character is stored into the char c. If the end of the dot file is
- * reached, the function puts an empty string in c.
- */
- function first() {
- index = 0;
- c = dot.charAt(0);
- }
+ this.fromBackup = null; // used to clean up after reconnect
+ this.toBackup = null;; // used to clean up after reconnect
- /**
- * Get the next character from the dot file.
- * The character is stored into the char c. If the end of the dot file is
- * reached, the function puts an empty string in c.
- */
- function next() {
- index++;
- c = dot.charAt(index);
- }
+ // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster
+ // by storing the original information we can revert to the original connection when the cluser is opened.
+ this.originalFromId = [];
+ this.originalToId = [];
- /**
- * Preview the next character from the dot file.
- * @return {String} cNext
- */
- function nextPreview() {
- return dot.charAt(index + 1);
- }
+ this.connected = false;
- /**
- * Test whether given character is alphabetic or numeric
- * @param {String} c
- * @return {Boolean} isAlphaNumeric
- */
- var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
- function isAlphaNumeric(c) {
- return regexAlphaNumeric.test(c);
+ this.widthFixed = false;
+ this.lengthFixed = false;
+
+ this.setProperties(properties);
+
+ this.controlNodesEnabled = false;
+ this.controlNodes = {from:null, to:null, positions:{}};
+ this.connectedNode = null;
}
/**
- * Merge all properties of object b into object b
- * @param {Object} a
- * @param {Object} b
- * @return {Object} a
+ * Set or overwrite properties for the edge
+ * @param {Object} properties an object with properties
+ * @param {Object} constants and object with default, global properties
*/
- function merge (a, b) {
- if (!a) {
- a = {};
+ Edge.prototype.setProperties = function(properties) {
+ if (!properties) {
+ return;
}
- if (b) {
- for (var name in b) {
- if (b.hasOwnProperty(name)) {
- a[name] = b[name];
- }
- }
- }
- return a;
- }
+ var fields = ['style','fontSize','fontFace','fontColor','fontFill','width',
+ 'widthSelectionMultiplier','hoverWidth','arrowScaleFactor','dash','inheritColor'
+ ];
+ util.selectiveDeepExtend(fields, this.options, properties);
- /**
- * Set a value in an object, where the provided parameter name can be a
- * path with nested parameters. For example:
- *
- * var obj = {a: 2};
- * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
- *
- * @param {Object} obj
- * @param {String} path A parameter name or dot-separated parameter path,
- * like "color.highlight.border".
- * @param {*} value
- */
- function setValue(obj, path, value) {
- var keys = path.split('.');
- var o = obj;
- while (keys.length) {
- var key = keys.shift();
- if (keys.length) {
- // this isn't the end point
- if (!o[key]) {
- o[key] = {};
- }
- o = o[key];
+ if (properties.from !== undefined) {this.fromId = properties.from;}
+ if (properties.to !== undefined) {this.toId = properties.to;}
+
+ if (properties.id !== undefined) {this.id = properties.id;}
+ if (properties.label !== undefined) {this.label = properties.label; this.dirtyLabel = true;}
+
+ if (properties.title !== undefined) {this.title = properties.title;}
+ if (properties.value !== undefined) {this.value = properties.value;}
+ if (properties.length !== undefined) {this.physics.springLength = properties.length;}
+
+ if (properties.color !== undefined) {
+ this.options.inheritColor = false;
+ if (util.isString(properties.color)) {
+ this.options.color.color = properties.color;
+ this.options.color.highlight = properties.color;
}
else {
- // this is the end point
- o[key] = value;
+ if (properties.color.color !== undefined) {this.options.color.color = properties.color.color;}
+ if (properties.color.highlight !== undefined) {this.options.color.highlight = properties.color.highlight;}
+ if (properties.color.hover !== undefined) {this.options.color.hover = properties.color.hover;}
}
}
- }
- /**
- * Add a node to a graph object. If there is already a node with
- * the same id, their attributes will be merged.
- * @param {Object} graph
- * @param {Object} node
- */
- function addNode(graph, node) {
- var i, len;
- var current = null;
+ // A node is connected when it has a from and to node.
+ this.connect();
- // find root graph (in case of subgraph)
- var graphs = [graph]; // list with all graphs from current graph to root graph
- var root = graph;
- while (root.parent) {
- graphs.push(root.parent);
- root = root.parent;
- }
+ this.widthFixed = this.widthFixed || (properties.width !== undefined);
+ this.lengthFixed = this.lengthFixed || (properties.length !== undefined);
- // find existing node (at root level) by its id
- if (root.nodes) {
- for (i = 0, len = root.nodes.length; i < len; i++) {
- if (node.id === root.nodes[i].id) {
- current = root.nodes[i];
- break;
- }
- }
- }
+ this.widthSelected = this.options.width* this.options.widthSelectionMultiplier;
- if (!current) {
- // this is a new node
- current = {
- id: node.id
- };
- if (graph.node) {
- // clone default attributes
- current.attr = merge(current.attr, graph.node);
- }
+ // set draw method based on style
+ switch (this.options.style) {
+ case 'line': this.draw = this._drawLine; break;
+ case 'arrow': this.draw = this._drawArrow; break;
+ case 'arrow-center': this.draw = this._drawArrowCenter; break;
+ case 'dash-line': this.draw = this._drawDashLine; break;
+ default: this.draw = this._drawLine; break;
}
+ };
- // add node to this (sub)graph and all its parent graphs
- for (i = graphs.length - 1; i >= 0; i--) {
- var g = graphs[i];
+ /**
+ * Connect an edge to its nodes
+ */
+ Edge.prototype.connect = function () {
+ this.disconnect();
- if (!g.nodes) {
- g.nodes = [];
+ this.from = this.network.nodes[this.fromId] || null;
+ this.to = this.network.nodes[this.toId] || null;
+ this.connected = (this.from && this.to);
+
+ if (this.connected) {
+ this.from.attachEdge(this);
+ this.to.attachEdge(this);
+ }
+ else {
+ if (this.from) {
+ this.from.detachEdge(this);
}
- if (g.nodes.indexOf(current) == -1) {
- g.nodes.push(current);
+ if (this.to) {
+ this.to.detachEdge(this);
}
}
-
- // merge attributes
- if (node.attr) {
- current.attr = merge(current.attr, node.attr);
- }
- }
+ };
/**
- * Add an edge to a graph object
- * @param {Object} graph
- * @param {Object} edge
+ * Disconnect an edge from its nodes
*/
- function addEdge(graph, edge) {
- if (!graph.edges) {
- graph.edges = [];
+ Edge.prototype.disconnect = function () {
+ if (this.from) {
+ this.from.detachEdge(this);
+ this.from = null;
}
- graph.edges.push(edge);
- if (graph.edge) {
- var attr = merge({}, graph.edge); // clone default attributes
- edge.attr = merge(attr, edge.attr); // merge attributes
+ if (this.to) {
+ this.to.detachEdge(this);
+ this.to = null;
}
- }
+
+ this.connected = false;
+ };
/**
- * Create an edge to a graph object
- * @param {Object} graph
- * @param {String | Number | Object} from
- * @param {String | Number | Object} to
- * @param {String} type
- * @param {Object | null} attr
- * @return {Object} edge
+ * get the title of this edge.
+ * @return {string} title The title of the edge, or undefined when no title
+ * has been set.
*/
- function createEdge(graph, from, to, type, attr) {
- var edge = {
- from: from,
- to: to,
- type: type
- };
-
- if (graph.edge) {
- edge.attr = merge({}, graph.edge); // clone default attributes
- }
- edge.attr = merge(edge.attr || {}, attr); // merge attributes
+ Edge.prototype.getTitle = function() {
+ return typeof this.title === "function" ? this.title() : this.title;
+ };
- return edge;
- }
/**
- * Get next token in the current dot file.
- * The token and token type are available as token and tokenType
+ * Retrieve the value of the edge. Can be undefined
+ * @return {Number} value
*/
- function getToken() {
- tokenType = TOKENTYPE.NULL;
- token = '';
+ Edge.prototype.getValue = function() {
+ return this.value;
+ };
- // skip over whitespaces
- while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
- next();
+ /**
+ * Adjust the value range of the edge. The edge will adjust it's width
+ * based on its value.
+ * @param {Number} min
+ * @param {Number} max
+ */
+ Edge.prototype.setValueRange = function(min, max) {
+ if (!this.widthFixed && this.value !== undefined) {
+ var scale = (this.options.widthMax - this.options.widthMin) / (max - min);
+ this.options.width= (this.value - min) * scale + this.options.widthMin;
+ this.widthSelected = this.options.width* this.options.widthSelectionMultiplier;
}
+ };
- do {
- var isComment = false;
+ /**
+ * Redraw a edge
+ * Draw this edge in the given canvas
+ * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
+ * @param {CanvasRenderingContext2D} ctx
+ */
+ Edge.prototype.draw = function(ctx) {
+ throw "Method draw not initialized in edge";
+ };
- // skip comment
- if (c == '#') {
- // find the previous non-space character
- var i = index - 1;
- while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') {
- i--;
- }
- if (dot.charAt(i) == '\n' || dot.charAt(i) == '') {
- // the # is at the start of a line, this is indeed a line comment
- while (c != '' && c != '\n') {
- next();
- }
- isComment = true;
- }
- }
- if (c == '/' && nextPreview() == '/') {
- // skip line comment
- while (c != '' && c != '\n') {
- next();
- }
- isComment = true;
- }
- if (c == '/' && nextPreview() == '*') {
- // skip block comment
- while (c != '') {
- if (c == '*' && nextPreview() == '/') {
- // end of block comment found. skip these last two characters
- next();
- next();
- break;
- }
- else {
- next();
- }
- }
- isComment = true;
- }
+ /**
+ * Check if this object is overlapping with the provided object
+ * @param {Object} obj an object with parameters left, top
+ * @return {boolean} True if location is located on the edge
+ */
+ Edge.prototype.isOverlappingWith = function(obj) {
+ if (this.connected) {
+ var distMax = 10;
+ var xFrom = this.from.x;
+ var yFrom = this.from.y;
+ var xTo = this.to.x;
+ var yTo = this.to.y;
+ var xObj = obj.left;
+ var yObj = obj.top;
- // skip over whitespaces
- while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
- next();
- }
- }
- while (isComment);
+ var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj);
- // check for end of dot file
- if (c == '') {
- // token is still empty
- tokenType = TOKENTYPE.DELIMITER;
- return;
+ return (dist < distMax);
}
-
- // check for delimiters consisting of 2 characters
- var c2 = c + nextPreview();
- if (DELIMITERS[c2]) {
- tokenType = TOKENTYPE.DELIMITER;
- token = c2;
- next();
- next();
- return;
+ else {
+ return false
}
+ };
- // check for delimiters consisting of 1 character
- if (DELIMITERS[c]) {
- tokenType = TOKENTYPE.DELIMITER;
- token = c;
- next();
- return;
+ Edge.prototype._getColor = function() {
+ var colorObj = this.options.color;
+ if (this.options.inheritColor == "to") {
+ colorObj = {
+ highlight: this.to.options.color.highlight.border,
+ hover: this.to.options.color.hover.border,
+ color: this.to.options.color.border
+ };
+ }
+ else if (this.options.inheritColor == "from" || this.options.inheritColor == true) {
+ colorObj = {
+ highlight: this.from.options.color.highlight.border,
+ hover: this.from.options.color.hover.border,
+ color: this.from.options.color.border
+ };
}
- // check for an identifier (number or string)
- // TODO: more precise parsing of numbers/strings (and the port separator ':')
- if (isAlphaNumeric(c) || c == '-') {
- token += c;
- next();
+ if (this.selected == true) {return colorObj.highlight;}
+ else if (this.hover == true) {return colorObj.hover;}
+ else {return colorObj.color;}
+ };
- while (isAlphaNumeric(c)) {
- token += c;
- next();
+
+ /**
+ * Redraw a edge as a line
+ * Draw this edge in the given canvas
+ * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
+ * @param {CanvasRenderingContext2D} ctx
+ * @private
+ */
+ Edge.prototype._drawLine = function(ctx) {
+ // set style
+ ctx.strokeStyle = this._getColor();
+ ctx.lineWidth = this._getLineWidth();
+
+ if (this.from != this.to) {
+ // draw line
+ var via = this._line(ctx);
+
+ // draw label
+ var point;
+ if (this.label) {
+ if (this.options.smoothCurves.enabled == true && via != null) {
+ var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
+ var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
+ point = {x:midpointX, y:midpointY};
+ }
+ else {
+ point = this._pointOnLine(0.5);
+ }
+ this._label(ctx, this.label, point.x, point.y);
}
- if (token == 'false') {
- token = false; // convert to boolean
+ }
+ else {
+ var x, y;
+ var radius = this.physics.springLength / 4;
+ var node = this.from;
+ if (!node.width) {
+ node.resize(ctx);
}
- else if (token == 'true') {
- token = true; // convert to boolean
+ if (node.width > node.height) {
+ x = node.x + node.width / 2;
+ y = node.y - radius;
}
- else if (!isNaN(Number(token))) {
- token = Number(token); // convert to number
+ else {
+ x = node.x + radius;
+ y = node.y - node.height / 2;
}
- tokenType = TOKENTYPE.IDENTIFIER;
- return;
+ this._circle(ctx, x, y, radius);
+ point = this._pointOnCircle(x, y, radius, 0.5);
+ this._label(ctx, this.label, point.x, point.y);
}
+ };
- // check for a string enclosed by double quotes
- if (c == '"') {
- next();
- while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
- token += c;
- if (c == '"') { // skip the escape character
- next();
- }
- next();
+ /**
+ * Get the line width of the edge. Depends on width and whether one of the
+ * connected nodes is selected.
+ * @return {Number} width
+ * @private
+ */
+ Edge.prototype._getLineWidth = function() {
+ if (this.selected == true) {
+ return Math.max(Math.min(this.widthSelected, this.options.widthMax), 0.3*this.networkScaleInv);
+ }
+ else {
+ if (this.hover == true) {
+ return Math.max(Math.min(this.options.hoverWidth, this.options.widthMax), 0.3*this.networkScaleInv);
}
- if (c != '"') {
- throw newSyntaxError('End of string " expected');
+ else {
+ return Math.max(this.options.width, 0.3*this.networkScaleInv);
}
- next();
- tokenType = TOKENTYPE.IDENTIFIER;
- return;
}
+ };
- // something unknown is found, wrong characters, a syntax error
- tokenType = TOKENTYPE.UNKNOWN;
- while (c != '') {
- token += c;
- next();
- }
- throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
- }
-
- /**
- * Parse a graph.
- * @returns {Object} graph
- */
- function parseGraph() {
- var graph = {};
-
- first();
- getToken();
-
- // optional strict keyword
- if (token == 'strict') {
- graph.strict = true;
- getToken();
- }
+ Edge.prototype._getViaCoordinates = function () {
+ var xVia = null;
+ var yVia = null;
+ var factor = this.options.smoothCurves.roundness;
+ var type = this.options.smoothCurves.type;
- // graph or digraph keyword
- if (token == 'graph' || token == 'digraph') {
- graph.type = token;
- getToken();
+ var dx = Math.abs(this.from.x - this.to.x);
+ var dy = Math.abs(this.from.y - this.to.y);
+ if (type == 'discrete' || type == 'diagonalCross') {
+ if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) {
+ if (this.from.y > this.to.y) {
+ if (this.from.x < this.to.x) {
+ xVia = this.from.x + factor * dy;
+ yVia = this.from.y - factor * dy;
+ }
+ else if (this.from.x > this.to.x) {
+ xVia = this.from.x - factor * dy;
+ yVia = this.from.y - factor * dy;
+ }
+ }
+ else if (this.from.y < this.to.y) {
+ if (this.from.x < this.to.x) {
+ xVia = this.from.x + factor * dy;
+ yVia = this.from.y + factor * dy;
+ }
+ else if (this.from.x > this.to.x) {
+ xVia = this.from.x - factor * dy;
+ yVia = this.from.y + factor * dy;
+ }
+ }
+ if (type == "discrete") {
+ xVia = dx < factor * dy ? this.from.x : xVia;
+ }
+ }
+ else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) {
+ if (this.from.y > this.to.y) {
+ if (this.from.x < this.to.x) {
+ xVia = this.from.x + factor * dx;
+ yVia = this.from.y - factor * dx;
+ }
+ else if (this.from.x > this.to.x) {
+ xVia = this.from.x - factor * dx;
+ yVia = this.from.y - factor * dx;
+ }
+ }
+ else if (this.from.y < this.to.y) {
+ if (this.from.x < this.to.x) {
+ xVia = this.from.x + factor * dx;
+ yVia = this.from.y + factor * dx;
+ }
+ else if (this.from.x > this.to.x) {
+ xVia = this.from.x - factor * dx;
+ yVia = this.from.y + factor * dx;
+ }
+ }
+ if (type == "discrete") {
+ yVia = dy < factor * dx ? this.from.y : yVia;
+ }
+ }
}
-
- // optional graph id
- if (tokenType == TOKENTYPE.IDENTIFIER) {
- graph.id = token;
- getToken();
+ else if (type == "straightCross") {
+ if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { // up - down
+ xVia = this.from.x;
+ if (this.from.y < this.to.y) {
+ yVia = this.to.y - (1-factor) * dy;
+ }
+ else {
+ yVia = this.to.y + (1-factor) * dy;
+ }
+ }
+ else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { // left - right
+ if (this.from.x < this.to.x) {
+ xVia = this.to.x - (1-factor) * dx;
+ }
+ else {
+ xVia = this.to.x + (1-factor) * dx;
+ }
+ yVia = this.from.y;
+ }
}
-
- // open angle bracket
- if (token != '{') {
- throw newSyntaxError('Angle bracket { expected');
+ else if (type == 'horizontal') {
+ if (this.from.x < this.to.x) {
+ xVia = this.to.x - (1-factor) * dx;
+ }
+ else {
+ xVia = this.to.x + (1-factor) * dx;
+ }
+ yVia = this.from.y;
}
- getToken();
-
- // statements
- parseStatements(graph);
-
- // close angle bracket
- if (token != '}') {
- throw newSyntaxError('Angle bracket } expected');
+ else if (type == 'vertical') {
+ xVia = this.from.x;
+ if (this.from.y < this.to.y) {
+ yVia = this.to.y - (1-factor) * dy;
+ }
+ else {
+ yVia = this.to.y + (1-factor) * dy;
+ }
}
- getToken();
-
- // end of file
- if (token !== '') {
- throw newSyntaxError('End of file expected');
+ else { // continuous
+ if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) {
+ if (this.from.y > this.to.y) {
+ if (this.from.x < this.to.x) {
+ // console.log(1)
+ xVia = this.from.x + factor * dy;
+ yVia = this.from.y - factor * dy;
+ xVia = this.to.x < xVia ? this.to.x : xVia;
+ }
+ else if (this.from.x > this.to.x) {
+ // console.log(2)
+ xVia = this.from.x - factor * dy;
+ yVia = this.from.y - factor * dy;
+ xVia = this.to.x > xVia ? this.to.x :xVia;
+ }
+ }
+ else if (this.from.y < this.to.y) {
+ if (this.from.x < this.to.x) {
+ // console.log(3)
+ xVia = this.from.x + factor * dy;
+ yVia = this.from.y + factor * dy;
+ xVia = this.to.x < xVia ? this.to.x : xVia;
+ }
+ else if (this.from.x > this.to.x) {
+ // console.log(4, this.from.x, this.to.x)
+ xVia = this.from.x - factor * dy;
+ yVia = this.from.y + factor * dy;
+ xVia = this.to.x > xVia ? this.to.x : xVia;
+ }
+ }
+ }
+ else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) {
+ if (this.from.y > this.to.y) {
+ if (this.from.x < this.to.x) {
+ // console.log(5)
+ xVia = this.from.x + factor * dx;
+ yVia = this.from.y - factor * dx;
+ yVia = this.to.y > yVia ? this.to.y : yVia;
+ }
+ else if (this.from.x > this.to.x) {
+ // console.log(6)
+ xVia = this.from.x - factor * dx;
+ yVia = this.from.y - factor * dx;
+ yVia = this.to.y > yVia ? this.to.y : yVia;
+ }
+ }
+ else if (this.from.y < this.to.y) {
+ if (this.from.x < this.to.x) {
+ // console.log(7)
+ xVia = this.from.x + factor * dx;
+ yVia = this.from.y + factor * dx;
+ yVia = this.to.y < yVia ? this.to.y : yVia;
+ }
+ else if (this.from.x > this.to.x) {
+ // console.log(8)
+ xVia = this.from.x - factor * dx;
+ yVia = this.from.y + factor * dx;
+ yVia = this.to.y < yVia ? this.to.y : yVia;
+ }
+ }
+ }
}
- getToken();
- // remove temporary default properties
- delete graph.node;
- delete graph.edge;
- delete graph.graph;
- return graph;
- }
+ return {x:xVia, y:yVia};
+ };
/**
- * Parse a list with statements.
- * @param {Object} graph
+ * Draw a line between two nodes
+ * @param {CanvasRenderingContext2D} ctx
+ * @private
*/
- function parseStatements (graph) {
- while (token !== '' && token != '}') {
- parseStatement(graph);
- if (token == ';') {
- getToken();
+ Edge.prototype._line = function (ctx) {
+ // draw a straight line
+ ctx.beginPath();
+ ctx.moveTo(this.from.x, this.from.y);
+ if (this.options.smoothCurves.enabled == true) {
+ if (this.options.smoothCurves.dynamic == false) {
+ var via = this._getViaCoordinates();
+ if (via.x == null) {
+ ctx.lineTo(this.to.x, this.to.y);
+ ctx.stroke();
+ return null;
+ }
+ else {
+ // this.via.x = via.x;
+ // this.via.y = via.y;
+ ctx.quadraticCurveTo(via.x,via.y,this.to.x, this.to.y);
+ ctx.stroke();
+ return via;
+ }
+ }
+ else {
+ ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
+ ctx.stroke();
+ return this.via;
}
}
- }
+ else {
+ ctx.lineTo(this.to.x, this.to.y);
+ ctx.stroke();
+ return null;
+ }
+ };
/**
- * Parse a single statement. Can be a an attribute statement, node
- * statement, a series of node statements and edge statements, or a
- * parameter.
- * @param {Object} graph
+ * Draw a line from a node to itself, a circle
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} radius
+ * @private
*/
- function parseStatement(graph) {
- // parse subgraph
- var subgraph = parseSubgraph(graph);
- if (subgraph) {
- // edge statements
- parseEdge(graph, subgraph);
+ Edge.prototype._circle = function (ctx, x, y, radius) {
+ // draw a circle
+ ctx.beginPath();
+ ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
+ ctx.stroke();
+ };
- return;
- }
+ /**
+ * Draw label with white background and with the middle at (x, y)
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {String} text
+ * @param {Number} x
+ * @param {Number} y
+ * @private
+ */
+ Edge.prototype._label = function (ctx, text, x, y) {
+ if (text) {
+ ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
+ this.options.fontSize + "px " + this.options.fontFace;
+ var yLine;
- // parse an attribute statement
- var attr = parseAttributeStatement(graph);
- if (attr) {
- return;
- }
+ if (this.dirtyLabel == true) {
+ var lines = String(text).split('\n');
+ var lineCount = lines.length;
+ var fontSize = (Number(this.options.fontSize) + 4);
+ yLine = y + (1 - lineCount) / 2 * fontSize;
- // parse node
- if (tokenType != TOKENTYPE.IDENTIFIER) {
- throw newSyntaxError('Identifier expected');
- }
- var id = token; // id can be a string or a number
- getToken();
+ var width = ctx.measureText(lines[0]).width;
+ for (var i = 1; i < lineCount; i++) {
+ var lineWidth = ctx.measureText(lines[i]).width;
+ width = lineWidth > width ? lineWidth : width;
+ }
+ var height = this.options.fontSize * lineCount;
+ var left = x - width / 2;
+ var top = y - height / 2;
- if (token == '=') {
- // id statement
- getToken();
- if (tokenType != TOKENTYPE.IDENTIFIER) {
- throw newSyntaxError('Identifier expected');
+ // cache
+ this.labelDimensions = {top:top,left:left,width:width,height:height,yLine:yLine};
+ }
+
+
+ if (this.options.fontFill !== undefined && this.options.fontFill !== null && this.options.fontFill !== "none") {
+ ctx.fillStyle = this.options.fontFill;
+ ctx.fillRect(this.labelDimensions.left,
+ this.labelDimensions.top,
+ this.labelDimensions.width,
+ this.labelDimensions.height);
+ }
+
+ // draw text
+ ctx.fillStyle = this.options.fontColor || "black";
+ ctx.textAlign = "center";
+ ctx.textBaseline = "middle";
+ yLine = this.labelDimensions.yLine;
+ for (var i = 0; i < lineCount; i++) {
+ ctx.fillText(lines[i], x, yLine);
+ yLine += fontSize;
}
- graph[id] = token;
- getToken();
- // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
- }
- else {
- parseNodeStatement(graph, id);
}
- }
+ };
/**
- * Parse a subgraph
- * @param {Object} graph parent graph object
- * @return {Object | null} subgraph
+ * Redraw a edge as a dashed line
+ * Draw this edge in the given canvas
+ * @author David Jordan
+ * @date 2012-08-08
+ * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
+ * @param {CanvasRenderingContext2D} ctx
+ * @private
*/
- function parseSubgraph (graph) {
- var subgraph = null;
+ Edge.prototype._drawDashLine = function(ctx) {
+ // set style
+ ctx.strokeStyle = this._getColor();
+ ctx.lineWidth = this._getLineWidth();
- // optional subgraph keyword
- if (token == 'subgraph') {
- subgraph = {};
- subgraph.type = 'subgraph';
- getToken();
+ var via = null;
+ // only firefox and chrome support this method, else we use the legacy one.
+ if (ctx.mozDash !== undefined || ctx.setLineDash !== undefined) {
+ // configure the dash pattern
+ var pattern = [0];
+ if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) {
+ pattern = [this.options.dash.length,this.options.dash.gap];
+ }
+ else {
+ pattern = [5,5];
+ }
- // optional graph id
- if (tokenType == TOKENTYPE.IDENTIFIER) {
- subgraph.id = token;
- getToken();
+ // set dash settings for chrome or firefox
+ if (typeof ctx.setLineDash !== 'undefined') { //Chrome
+ ctx.setLineDash(pattern);
+ ctx.lineDashOffset = 0;
+
+ } else { //Firefox
+ ctx.mozDash = pattern;
+ ctx.mozDashOffset = 0;
+ }
+
+ // draw the line
+ via = this._line(ctx);
+
+ // restore the dash settings.
+ if (typeof ctx.setLineDash !== 'undefined') { //Chrome
+ ctx.setLineDash([0]);
+ ctx.lineDashOffset = 0;
+
+ } else { //Firefox
+ ctx.mozDash = [0];
+ ctx.mozDashOffset = 0;
+ }
+ }
+ else { // unsupporting smooth lines
+ // draw dashed line
+ ctx.beginPath();
+ ctx.lineCap = 'round';
+ if (this.options.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value
+ {
+ ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
+ [this.options.dash.length,this.options.dash.gap,this.options.dash.altLength,this.options.dash.gap]);
+ }
+ else if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) //If a dash and gap value has been set add to the array this value
+ {
+ ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
+ [this.options.dash.length,this.options.dash.gap]);
+ }
+ else //If all else fails draw a line
+ {
+ ctx.moveTo(this.from.x, this.from.y);
+ ctx.lineTo(this.to.x, this.to.y);
}
+ ctx.stroke();
}
- // open angle bracket
- if (token == '{') {
- getToken();
-
- if (!subgraph) {
- subgraph = {};
- }
- subgraph.parent = graph;
- subgraph.node = graph.node;
- subgraph.edge = graph.edge;
- subgraph.graph = graph.graph;
-
- // statements
- parseStatements(subgraph);
-
- // close angle bracket
- if (token != '}') {
- throw newSyntaxError('Angle bracket } expected');
+ // draw label
+ if (this.label) {
+ var point;
+ if (this.options.smoothCurves.enabled == true && via != null) {
+ var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
+ var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
+ point = {x:midpointX, y:midpointY};
}
- getToken();
-
- // remove temporary default properties
- delete subgraph.node;
- delete subgraph.edge;
- delete subgraph.graph;
- delete subgraph.parent;
-
- // register at the parent graph
- if (!graph.subgraphs) {
- graph.subgraphs = [];
+ else {
+ point = this._pointOnLine(0.5);
}
- graph.subgraphs.push(subgraph);
+ this._label(ctx, this.label, point.x, point.y);
}
-
- return subgraph;
- }
+ };
/**
- * parse an attribute statement like "node [shape=circle fontSize=16]".
- * Available keywords are 'node', 'edge', 'graph'.
- * The previous list with default attributes will be replaced
- * @param {Object} graph
- * @returns {String | null} keyword Returns the name of the parsed attribute
- * (node, edge, graph), or null if nothing
- * is parsed.
+ * Get a point on a line
+ * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
+ * @return {Object} point
+ * @private
*/
- function parseAttributeStatement (graph) {
- // attribute statements
- if (token == 'node') {
- getToken();
-
- // node attributes
- graph.node = parseAttributeList();
- return 'node';
- }
- else if (token == 'edge') {
- getToken();
-
- // edge attributes
- graph.edge = parseAttributeList();
- return 'edge';
- }
- else if (token == 'graph') {
- getToken();
-
- // graph attributes
- graph.graph = parseAttributeList();
- return 'graph';
+ Edge.prototype._pointOnLine = function (percentage) {
+ return {
+ x: (1 - percentage) * this.from.x + percentage * this.to.x,
+ y: (1 - percentage) * this.from.y + percentage * this.to.y
}
-
- return null;
- }
+ };
/**
- * parse a node statement
- * @param {Object} graph
- * @param {String | Number} id
+ * Get a point on a circle
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} radius
+ * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
+ * @return {Object} point
+ * @private
*/
- function parseNodeStatement(graph, id) {
- // node statement
- var node = {
- id: id
- };
- var attr = parseAttributeList();
- if (attr) {
- node.attr = attr;
+ Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
+ var angle = (percentage - 3/8) * 2 * Math.PI;
+ return {
+ x: x + radius * Math.cos(angle),
+ y: y - radius * Math.sin(angle)
}
- addNode(graph, node);
-
- // edge statements
- parseEdge(graph, id);
- }
+ };
/**
- * Parse an edge or a series of edges
- * @param {Object} graph
- * @param {String | Number} from Id of the from node
+ * Redraw a edge as a line with an arrow halfway the line
+ * Draw this edge in the given canvas
+ * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
+ * @param {CanvasRenderingContext2D} ctx
+ * @private
*/
- function parseEdge(graph, from) {
- while (token == '->' || token == '--') {
- var to;
- var type = token;
- getToken();
+ Edge.prototype._drawArrowCenter = function(ctx) {
+ var point;
+ // set style
+ ctx.strokeStyle = this._getColor();
+ ctx.fillStyle = ctx.strokeStyle;
+ ctx.lineWidth = this._getLineWidth();
- var subgraph = parseSubgraph(graph);
- if (subgraph) {
- to = subgraph;
+ if (this.from != this.to) {
+ // draw line
+ var via = this._line(ctx);
+
+ var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
+ var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
+ // draw an arrow halfway the line
+ if (this.options.smoothCurves.enabled == true && via != null) {
+ var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
+ var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
+ point = {x:midpointX, y:midpointY};
}
else {
- if (tokenType != TOKENTYPE.IDENTIFIER) {
- throw newSyntaxError('Identifier or subgraph expected');
- }
- to = token;
- addNode(graph, {
- id: to
- });
- getToken();
+ point = this._pointOnLine(0.5);
}
- // parse edge attributes
- var attr = parseAttributeList();
+ ctx.arrow(point.x, point.y, angle, length);
+ ctx.fill();
+ ctx.stroke();
- // create edge
- var edge = createEdge(graph, from, to, type, attr);
- addEdge(graph, edge);
+ // draw label
+ if (this.label) {
+ this._label(ctx, this.label, point.x, point.y);
+ }
+ }
+ else {
+ // draw circle
+ var x, y;
+ var radius = 0.25 * Math.max(100,this.physics.springLength);
+ var node = this.from;
+ if (!node.width) {
+ node.resize(ctx);
+ }
+ if (node.width > node.height) {
+ x = node.x + node.width * 0.5;
+ y = node.y - radius;
+ }
+ else {
+ x = node.x + radius;
+ y = node.y - node.height * 0.5;
+ }
+ this._circle(ctx, x, y, radius);
- from = to;
+ // draw all arrows
+ var angle = 0.2 * Math.PI;
+ var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
+ point = this._pointOnCircle(x, y, radius, 0.5);
+ ctx.arrow(point.x, point.y, angle, length);
+ ctx.fill();
+ ctx.stroke();
+
+ // draw label
+ if (this.label) {
+ point = this._pointOnCircle(x, y, radius, 0.5);
+ this._label(ctx, this.label, point.x, point.y);
+ }
}
- }
+ };
+
+
/**
- * Parse a set with attributes,
- * for example [label="1.000", shape=solid]
- * @return {Object | null} attr
+ * Redraw a edge as a line with an arrow
+ * Draw this edge in the given canvas
+ * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
+ * @param {CanvasRenderingContext2D} ctx
+ * @private
*/
- function parseAttributeList() {
- var attr = null;
-
- while (token == '[') {
- getToken();
- attr = {};
- while (token !== '' && token != ']') {
- if (tokenType != TOKENTYPE.IDENTIFIER) {
- throw newSyntaxError('Attribute name expected');
- }
- var name = token;
+ Edge.prototype._drawArrow = function(ctx) {
+ // set style
+ ctx.strokeStyle = this._getColor();
+ ctx.fillStyle = ctx.strokeStyle;
+ ctx.lineWidth = this._getLineWidth();
- getToken();
- if (token != '=') {
- throw newSyntaxError('Equal sign = expected');
- }
- getToken();
+ var angle, length;
+ //draw a line
+ if (this.from != this.to) {
+ angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
+ var dx = (this.to.x - this.from.x);
+ var dy = (this.to.y - this.from.y);
+ var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
- if (tokenType != TOKENTYPE.IDENTIFIER) {
- throw newSyntaxError('Attribute value expected');
- }
- var value = token;
- setValue(attr, name, value); // name can be a path
+ var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
+ var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
+ var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
+ var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
- getToken();
- if (token ==',') {
- getToken();
- }
+ var via;
+ if (this.options.smoothCurves.dynamic == true && this.options.smoothCurves.enabled == true ) {
+ via = this.via;
+ }
+ else if (this.options.smoothCurves.enabled == true) {
+ via = this._getViaCoordinates();
}
- if (token != ']') {
- throw newSyntaxError('Bracket ] expected');
+ if (this.options.smoothCurves.enabled == true && via.x != null) {
+ angle = Math.atan2((this.to.y - via.y), (this.to.x - via.x));
+ dx = (this.to.x - via.x);
+ dy = (this.to.y - via.y);
+ edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
}
- getToken();
- }
+ var toBorderDist = this.to.distanceToBorder(ctx, angle);
+ var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
- return attr;
- }
+ var xTo,yTo;
+ if (this.options.smoothCurves.enabled == true && via.x != null) {
+ xTo = (1 - toBorderPoint) * via.x + toBorderPoint * this.to.x;
+ yTo = (1 - toBorderPoint) * via.y + toBorderPoint * this.to.y;
+ }
+ else {
+ xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
+ yTo = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
+ }
- /**
- * Create a syntax error with extra information on current token and index.
- * @param {String} message
- * @returns {SyntaxError} err
- */
- function newSyntaxError(message) {
- return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
- }
+ ctx.beginPath();
+ ctx.moveTo(xFrom,yFrom);
+ if (this.options.smoothCurves.enabled == true && via.x != null) {
+ ctx.quadraticCurveTo(via.x,via.y,xTo, yTo);
+ }
+ else {
+ ctx.lineTo(xTo, yTo);
+ }
+ ctx.stroke();
- /**
- * Chop off text after a maximum length
- * @param {String} text
- * @param {Number} maxLength
- * @returns {String}
- */
- function chop (text, maxLength) {
- return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
- }
+ // draw arrow at the end of the line
+ length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
+ ctx.arrow(xTo, yTo, angle, length);
+ ctx.fill();
+ ctx.stroke();
- /**
- * Execute a function fn for each pair of elements in two arrays
- * @param {Array | *} array1
- * @param {Array | *} array2
- * @param {function} fn
- */
- function forEach2(array1, array2, fn) {
- if (Array.isArray(array1)) {
- array1.forEach(function (elem1) {
- if (Array.isArray(array2)) {
- array2.forEach(function (elem2) {
- fn(elem1, elem2);
- });
+ // draw label
+ if (this.label) {
+ var point;
+ if (this.options.smoothCurves.enabled == true && via != null) {
+ var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
+ var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
+ point = {x:midpointX, y:midpointY};
}
else {
- fn(elem1, array2);
+ point = this._pointOnLine(0.5);
}
- });
+ this._label(ctx, this.label, point.x, point.y);
+ }
}
else {
- if (Array.isArray(array2)) {
- array2.forEach(function (elem2) {
- fn(array1, elem2);
- });
+ // draw circle
+ var node = this.from;
+ var x, y, arrow;
+ var radius = 0.25 * Math.max(100,this.physics.springLength);
+ if (!node.width) {
+ node.resize(ctx);
+ }
+ if (node.width > node.height) {
+ x = node.x + node.width * 0.5;
+ y = node.y - radius;
+ arrow = {
+ x: x,
+ y: node.y,
+ angle: 0.9 * Math.PI
+ };
}
else {
- fn(array1, array2);
+ x = node.x + radius;
+ y = node.y - node.height * 0.5;
+ arrow = {
+ x: node.x,
+ y: y,
+ angle: 0.6 * Math.PI
+ };
}
- }
- }
+ ctx.beginPath();
+ // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
+ ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
+ ctx.stroke();
- /**
- * Convert a string containing a graph in DOT language into a map containing
- * with nodes and edges in the format of graph.
- * @param {String} data Text containing a graph in DOT-notation
- * @return {Object} graphData
- */
- function DOTToGraph (data) {
- // parse the DOT file
- var dotData = parseDOT(data);
- var graphData = {
- nodes: [],
- edges: [],
- options: {}
- };
+ // draw all arrows
+ var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
+ ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
+ ctx.fill();
+ ctx.stroke();
- // copy the nodes
- if (dotData.nodes) {
- dotData.nodes.forEach(function (dotNode) {
- var graphNode = {
- id: dotNode.id,
- label: String(dotNode.label || dotNode.id)
- };
- merge(graphNode, dotNode.attr);
- if (graphNode.image) {
- graphNode.shape = 'image';
- }
- graphData.nodes.push(graphNode);
- });
+ // draw label
+ if (this.label) {
+ point = this._pointOnCircle(x, y, radius, 0.5);
+ this._label(ctx, this.label, point.x, point.y);
+ }
}
+ };
- // copy the edges
- if (dotData.edges) {
- /**
- * Convert an edge in DOT format to an edge with VisGraph format
- * @param {Object} dotEdge
- * @returns {Object} graphEdge
- */
- var convertEdge = function (dotEdge) {
- var graphEdge = {
- from: dotEdge.from,
- to: dotEdge.to
- };
- merge(graphEdge, dotEdge.attr);
- graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
- return graphEdge;
- }
- dotData.edges.forEach(function (dotEdge) {
- var from, to;
- if (dotEdge.from instanceof Object) {
- from = dotEdge.from.nodes;
- }
- else {
- from = {
- id: dotEdge.from
- }
- }
- if (dotEdge.to instanceof Object) {
- to = dotEdge.to.nodes;
+ /**
+ * Calculate the distance between a point (x3,y3) and a line segment from
+ * (x1,y1) to (x2,y2).
+ * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
+ * @param {number} x1
+ * @param {number} y1
+ * @param {number} x2
+ * @param {number} y2
+ * @param {number} x3
+ * @param {number} y3
+ * @private
+ */
+ Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
+ var returnValue = 0;
+ if (this.from != this.to) {
+ if (this.options.smoothCurves.enabled == true) {
+ var xVia, yVia;
+ if (this.options.smoothCurves.enabled == true && this.options.smoothCurves.dynamic == true) {
+ xVia = this.via.x;
+ yVia = this.via.y;
}
else {
- to = {
- id: dotEdge.to
- }
- }
-
- if (dotEdge.from instanceof Object && dotEdge.from.edges) {
- dotEdge.from.edges.forEach(function (subEdge) {
- var graphEdge = convertEdge(subEdge);
- graphData.edges.push(graphEdge);
- });
- }
-
- forEach2(from, to, function (from, to) {
- var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
- var graphEdge = convertEdge(subEdge);
- graphData.edges.push(graphEdge);
- });
-
- if (dotEdge.to instanceof Object && dotEdge.to.edges) {
- dotEdge.to.edges.forEach(function (subEdge) {
- var graphEdge = convertEdge(subEdge);
- graphData.edges.push(graphEdge);
- });
- }
- });
- }
-
- // copy the options
- if (dotData.attr) {
- graphData.options = dotData.attr;
- }
-
- return graphData;
- }
-
- // exports
- exports.parseDOT = parseDOT;
- exports.DOTToGraph = DOTToGraph;
-
-
-/***/ },
-/* 53 */
-/***/ function(module, exports, __webpack_require__) {
-
-
- function parseGephi(gephiJSON, options) {
- var edges = [];
- var nodes = [];
- this.options = {
- edges: {
- inheritColor: true
- },
- nodes: {
- allowedToMove: false,
- parseColor: false
+ var via = this._getViaCoordinates();
+ xVia = via.x;
+ yVia = via.y;
+ }
+ var minDistance = 1e9;
+ var distance;
+ var i,t,x,y, lastX, lastY;
+ for (i = 0; i < 10; i++) {
+ t = 0.1*i;
+ x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*xVia + Math.pow(t,2)*x2;
+ y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*yVia + Math.pow(t,2)*y2;
+ if (i > 0) {
+ distance = this._getDistanceToLine(lastX,lastY,x,y, x3,y3);
+ minDistance = distance < minDistance ? distance : minDistance;
+ }
+ lastX = x; lastY = y;
+ }
+ returnValue = minDistance;
+ }
+ else {
+ returnValue = this._getDistanceToLine(x1,y1,x2,y2,x3,y3);
}
- };
-
- if (options !== undefined) {
- this.options.nodes['allowedToMove'] = options.allowedToMove | false;
- this.options.nodes['parseColor'] = options.parseColor | false;
- this.options.edges['inheritColor'] = options.inheritColor | true;
- }
-
- var gEdges = gephiJSON.edges;
- var gNodes = gephiJSON.nodes;
- for (var i = 0; i < gEdges.length; i++) {
- var edge = {};
- var gEdge = gEdges[i];
- edge['id'] = gEdge.id;
- edge['from'] = gEdge.source;
- edge['to'] = gEdge.target;
- edge['attributes'] = gEdge.attributes;
- // edge['value'] = gEdge.attributes !== undefined ? gEdge.attributes.Weight : undefined;
- // edge['width'] = edge['value'] !== undefined ? undefined : edgegEdge.size;
- edge['color'] = gEdge.color;
- edge['inheritColor'] = edge['color'] !== undefined ? false : this.options.inheritColor;
- edges.push(edge);
}
-
- for (var i = 0; i < gNodes.length; i++) {
- var node = {};
- var gNode = gNodes[i];
- node['id'] = gNode.id;
- node['attributes'] = gNode.attributes;
- node['x'] = gNode.x;
- node['y'] = gNode.y;
- node['label'] = gNode.label;
- if (this.options.nodes.parseColor == true) {
- node['color'] = gNode.color;
+ else {
+ var x, y, dx, dy;
+ var radius = 0.25 * this.physics.springLength;
+ var node = this.from;
+ if (node.width > node.height) {
+ x = node.x + 0.5 * node.width;
+ y = node.y - radius;
}
else {
- node['color'] = gNode.color !== undefined ? {background:gNode.color, border:gNode.color} : undefined;
+ x = node.x + radius;
+ y = node.y - 0.5 * node.height;
}
- node['radius'] = gNode.size;
- node['allowedToMoveX'] = this.options.nodes.allowedToMove;
- node['allowedToMoveY'] = this.options.nodes.allowedToMove;
- nodes.push(node);
+ dx = x - x3;
+ dy = y - y3;
+ returnValue = Math.abs(Math.sqrt(dx*dx + dy*dy) - radius);
}
- return {nodes:nodes, edges:edges};
- }
+ if (this.labelDimensions.left < x3 &&
+ this.labelDimensions.left + this.labelDimensions.width > x3 &&
+ this.labelDimensions.top < y3 &&
+ this.labelDimensions.top + this.labelDimensions.height > y3) {
+ return 0;
+ }
+ else {
+ return returnValue;
+ }
+ };
- exports.parseGephi = parseGephi;
+ Edge.prototype._getDistanceToLine = function(x1,y1,x2,y2,x3,y3) {
+ var px = x2-x1,
+ py = y2-y1,
+ something = px*px + py*py,
+ u = ((x3 - x1) * px + (y3 - y1) * py) / something;
-/***/ },
-/* 54 */
-/***/ function(module, exports, __webpack_require__) {
+ if (u > 1) {
+ u = 1;
+ }
+ else if (u < 0) {
+ u = 0;
+ }
- var util = __webpack_require__(1);
+ var x = x1 + u * px,
+ y = y1 + u * py,
+ dx = x - x3,
+ dy = y - y3;
- /**
- * @class Groups
- * This class can store groups and properties specific for groups.
- */
- function Groups() {
- this.clear();
- this.defaultIndex = 0;
- }
+ //# Note: If the actual distance does not matter,
+ //# if you only want to compare what this function
+ //# returns to other results of this function, you
+ //# can just return the squared distance instead
+ //# (i.e. remove the sqrt) to gain a little performance
+ return Math.sqrt(dx*dx + dy*dy);
+ };
/**
- * default constants for group colors
+ * This allows the zoom level of the network to influence the rendering
+ *
+ * @param scale
*/
- Groups.DEFAULT = [
- {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}, hover: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
- {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}, hover: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
- {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}, hover: {border: "#FA0A10", background: "#FFAFB1"}}, // red
- {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}, hover: {border: "#41A906", background: "#A1EC76"}}, // green
- {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}, hover: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
- {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}, hover: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
- {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}, hover: {border: "#C37F00", background: "#FFCA66"}}, // orange
- {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}, hover: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
- {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}, hover: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
- {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}, hover: {border: "#4AD63A", background: "#E6FFE3"}} // mint
- ];
+ Edge.prototype.setScale = function(scale) {
+ this.networkScaleInv = 1.0/scale;
+ };
- /**
- * Clear all groups
- */
- Groups.prototype.clear = function () {
- this.groups = {};
- this.groups.length = function()
- {
- var i = 0;
- for ( var p in this ) {
- if (this.hasOwnProperty(p)) {
- i++;
- }
- }
- return i;
- }
+ Edge.prototype.select = function() {
+ this.selected = true;
+ };
+
+ Edge.prototype.unselect = function() {
+ this.selected = false;
};
+ Edge.prototype.positionBezierNode = function() {
+ if (this.via !== null && this.from !== null && this.to !== null) {
+ this.via.x = 0.5 * (this.from.x + this.to.x);
+ this.via.y = 0.5 * (this.from.y + this.to.y);
+ }
+ else {
+ this.via.x = 0;
+ this.via.y = 0;
+ }
+ };
/**
- * get group properties of a groupname. If groupname is not found, a new group
- * is added.
- * @param {*} groupname Can be a number, string, Date, etc.
- * @return {Object} group The created group, containing all group properties
+ * This function draws the control nodes for the manipulator.
+ * In order to enable this, only set the this.controlNodesEnabled to true.
+ * @param ctx
*/
- Groups.prototype.get = function (groupname) {
- var group = this.groups[groupname];
- if (group == undefined) {
- // create new group
- var index = this.defaultIndex % Groups.DEFAULT.length;
- this.defaultIndex++;
- group = {};
- group.color = Groups.DEFAULT[index];
- this.groups[groupname] = group;
- }
+ Edge.prototype._drawControlNodes = function(ctx) {
+ if (this.controlNodesEnabled == true) {
+ if (this.controlNodes.from === null && this.controlNodes.to === null) {
+ var nodeIdFrom = "edgeIdFrom:".concat(this.id);
+ var nodeIdTo = "edgeIdTo:".concat(this.id);
+ var constants = {
+ nodes:{group:'', radius:8},
+ physics:{damping:0},
+ clustering: {maxNodeSizeIncrements: 0 ,nodeScaling: {width:0, height: 0, radius:0}}
+ };
+ this.controlNodes.from = new Node(
+ {id:nodeIdFrom,
+ shape:'dot',
+ color:{background:'#ff4e00', border:'#3c3c3c', highlight: {background:'#07f968'}}
+ },{},{},constants);
+ this.controlNodes.to = new Node(
+ {id:nodeIdTo,
+ shape:'dot',
+ color:{background:'#ff4e00', border:'#3c3c3c', highlight: {background:'#07f968'}}
+ },{},{},constants);
+ }
- return group;
+ if (this.controlNodes.from.selected == false && this.controlNodes.to.selected == false) {
+ this.controlNodes.positions = this.getControlNodePositions(ctx);
+ this.controlNodes.from.x = this.controlNodes.positions.from.x;
+ this.controlNodes.from.y = this.controlNodes.positions.from.y;
+ this.controlNodes.to.x = this.controlNodes.positions.to.x;
+ this.controlNodes.to.y = this.controlNodes.positions.to.y;
+ }
+
+ this.controlNodes.from.draw(ctx);
+ this.controlNodes.to.draw(ctx);
+ }
+ else {
+ this.controlNodes = {from:null, to:null, positions:{}};
+ }
};
/**
- * Add a custom group style
- * @param {String} groupname
- * @param {Object} style An object containing borderColor,
- * backgroundColor, etc.
- * @return {Object} group The created group object
+ * Enable control nodes.
+ * @private
*/
- Groups.prototype.add = function (groupname, style) {
- this.groups[groupname] = style;
- return style;
+ Edge.prototype._enableControlNodes = function() {
+ this.fromBackup = this.from;
+ this.toBackup = this.to;
+ this.controlNodesEnabled = true;
};
- module.exports = Groups;
-
+ /**
+ * disable control nodes and remove from dynamicEdges from old node
+ * @private
+ */
+ Edge.prototype._disableControlNodes = function() {
+ this.fromId = this.from.id;
+ this.toId = this.to.id;
+ if (this.fromId != this.fromBackup.id) { // from was changed, remove edge from old 'from' node dynamic edges
+ this.fromBackup.detachEdge(this);
+ }
+ else if (this.toId != this.toBackup.id) { // to was changed, remove edge from old 'to' node dynamic edges
+ this.toBackup.detachEdge(this);
+ }
+
+ this.fromBackup = null;
+ this.toBackup = null;
+ this.controlNodesEnabled = false;
+ };
-/***/ },
-/* 55 */
-/***/ function(module, exports, __webpack_require__) {
/**
- * @class Images
- * This class loads images and keeps them stored.
+ * This checks if one of the control nodes is selected and if so, returns the control node object. Else it returns null.
+ * @param x
+ * @param y
+ * @returns {null}
+ * @private
*/
- function Images() {
- this.images = {};
+ Edge.prototype._getSelectedControlNode = function(x,y) {
+ var positions = this.controlNodes.positions;
+ var fromDistance = Math.sqrt(Math.pow(x - positions.from.x,2) + Math.pow(y - positions.from.y,2));
+ var toDistance = Math.sqrt(Math.pow(x - positions.to.x ,2) + Math.pow(y - positions.to.y ,2));
+
+ if (fromDistance < 15) {
+ this.connectedNode = this.from;
+ this.from = this.controlNodes.from;
+ return this.controlNodes.from;
+ }
+ else if (toDistance < 15) {
+ this.connectedNode = this.to;
+ this.to = this.controlNodes.to;
+ return this.controlNodes.to;
+ }
+ else {
+ return null;
+ }
+ };
- this.callback = undefined;
- }
/**
- * Set an onload callback function. This will be called each time an image
- * is loaded
- * @param {function} callback
+ * this resets the control nodes to their original position.
+ * @private
*/
- Images.prototype.setOnloadCallback = function(callback) {
- this.callback = callback;
+ Edge.prototype._restoreControlNodes = function() {
+ if (this.controlNodes.from.selected == true) {
+ this.from = this.connectedNode;
+ this.connectedNode = null;
+ this.controlNodes.from.unselect();
+ }
+ else if (this.controlNodes.to.selected == true) {
+ this.to = this.connectedNode;
+ this.connectedNode = null;
+ this.controlNodes.to.unselect();
+ }
};
/**
+ * this calculates the position of the control nodes on the edges of the parent nodes.
*
- * @param {string} url Url of the image
- * @param {string} url Url of an image to use if the url image is not found
- * @return {Image} img The image object
+ * @param ctx
+ * @returns {{from: {x: number, y: number}, to: {x: *, y: *}}}
*/
- Images.prototype.load = function(url, brokenUrl) {
- var img = this.images[url];
- if (img == undefined) {
- // create the image
- var images = this;
- img = new Image();
- this.images[url] = img;
- img.onload = function() {
- if (images.callback) {
- images.callback(this);
- }
- };
-
- img.onerror = function () {
- this.src = brokenUrl;
- if (images.callback) {
- images.callback(this);
- }
- };
-
- img.src = url;
+ Edge.prototype.getControlNodePositions = function(ctx) {
+ var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
+ var dx = (this.to.x - this.from.x);
+ var dy = (this.to.y - this.from.y);
+ var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
+ var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
+ var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
+ var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
+ var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
+
+ var via;
+ if (this.options.smoothCurves.dynamic == true && this.options.smoothCurves.enabled == true) {
+ via = this.via;
+ }
+ else if (this.options.smoothCurves.enabled == true) {
+ via = this._getViaCoordinates();
}
- return img;
- };
+ if (this.options.smoothCurves.enabled == true && via.x != null) {
+ angle = Math.atan2((this.to.y - via.y), (this.to.x - via.x));
+ dx = (this.to.x - via.x);
+ dy = (this.to.y - via.y);
+ edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
+ }
+ var toBorderDist = this.to.distanceToBorder(ctx, angle);
+ var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
- module.exports = Images;
+ var xTo,yTo;
+ if (this.options.smoothCurves.enabled == true && via.x != null) {
+ xTo = (1 - toBorderPoint) * via.x + toBorderPoint * this.to.x;
+ yTo = (1 - toBorderPoint) * via.y + toBorderPoint * this.to.y;
+ }
+ else {
+ xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
+ yTo = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
+ }
+
+ return {from:{x:xFrom,y:yFrom},to:{x:xTo,y:yTo}};
+ };
+ module.exports = Edge;
/***/ },
-/* 56 */
+/* 53 */
/***/ function(module, exports, __webpack_require__) {
var util = __webpack_require__(1);
@@ -27193,1409 +27372,1235 @@ return /******/ (function(modules) { // webpackBootstrap
Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) {
this.networkScaleInv = 1.0/scale;
this.networkScale = scale;
- this.canvasTopLeft = canvasTopLeft;
- this.canvasBottomRight = canvasBottomRight;
- };
-
-
- /**
- * This allows the zoom level of the network to influence the rendering
- *
- * @param scale
- */
- Node.prototype.setScale = function(scale) {
- this.networkScaleInv = 1.0/scale;
- this.networkScale = scale;
- };
-
-
-
- /**
- * set the velocity at 0. Is called when this node is contained in another during clustering
- */
- Node.prototype.clearVelocity = function() {
- this.vx = 0;
- this.vy = 0;
- };
-
-
- /**
- * Basic preservation of (kinectic) energy
- *
- * @param massBeforeClustering
- */
- Node.prototype.updateVelocity = function(massBeforeClustering) {
- var energyBefore = this.vx * this.vx * massBeforeClustering;
- //this.vx = (this.vx < 0) ? -Math.sqrt(energyBefore/this.options.mass) : Math.sqrt(energyBefore/this.options.mass);
- this.vx = Math.sqrt(energyBefore/this.options.mass);
- energyBefore = this.vy * this.vy * massBeforeClustering;
- //this.vy = (this.vy < 0) ? -Math.sqrt(energyBefore/this.options.mass) : Math.sqrt(energyBefore/this.options.mass);
- this.vy = Math.sqrt(energyBefore/this.options.mass);
- };
-
- module.exports = Node;
-
-
-/***/ },
-/* 57 */
-/***/ function(module, exports, __webpack_require__) {
-
- var util = __webpack_require__(1);
- var Node = __webpack_require__(56);
-
- /**
- * @class Edge
- *
- * A edge connects two nodes
- * @param {Object} properties Object with properties. Must contain
- * At least properties from and to.
- * Available properties: from (number),
- * to (number), label (string, color (string),
- * width (number), style (string),
- * length (number), title (string)
- * @param {Network} network A Network object, used to find and edge to
- * nodes.
- * @param {Object} constants An object with default values for
- * example for the color
- */
- function Edge (properties, network, networkConstants) {
- if (!network) {
- throw "No network provided";
- }
- var fields = ['edges','physics'];
- var constants = util.selectiveBridgeObject(fields,networkConstants);
- this.options = constants.edges;
- this.physics = constants.physics;
- this.options['smoothCurves'] = networkConstants['smoothCurves'];
-
-
- this.network = network;
-
- // initialize variables
- this.id = undefined;
- this.fromId = undefined;
- this.toId = undefined;
- this.title = undefined;
- this.widthSelected = this.options.width * this.options.widthSelectionMultiplier;
- this.value = undefined;
- this.selected = false;
- this.hover = false;
- this.labelDimensions = {top:0,left:0,width:0,height:0,yLine:0}; // could be cached
- this.dirtyLabel = true;
-
- this.from = null; // a node
- this.to = null; // a node
- this.via = null; // a temp node
-
- this.fromBackup = null; // used to clean up after reconnect
- this.toBackup = null;; // used to clean up after reconnect
-
- // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster
- // by storing the original information we can revert to the original connection when the cluser is opened.
- this.originalFromId = [];
- this.originalToId = [];
-
- this.connected = false;
-
- this.widthFixed = false;
- this.lengthFixed = false;
-
- this.setProperties(properties);
-
- this.controlNodesEnabled = false;
- this.controlNodes = {from:null, to:null, positions:{}};
- this.connectedNode = null;
- }
-
- /**
- * Set or overwrite properties for the edge
- * @param {Object} properties an object with properties
- * @param {Object} constants and object with default, global properties
- */
- Edge.prototype.setProperties = function(properties) {
- if (!properties) {
- return;
- }
-
- var fields = ['style','fontSize','fontFace','fontColor','fontFill','width',
- 'widthSelectionMultiplier','hoverWidth','arrowScaleFactor','dash','inheritColor'
- ];
- util.selectiveDeepExtend(fields, this.options, properties);
-
- if (properties.from !== undefined) {this.fromId = properties.from;}
- if (properties.to !== undefined) {this.toId = properties.to;}
-
- if (properties.id !== undefined) {this.id = properties.id;}
- if (properties.label !== undefined) {this.label = properties.label; this.dirtyLabel = true;}
-
- if (properties.title !== undefined) {this.title = properties.title;}
- if (properties.value !== undefined) {this.value = properties.value;}
- if (properties.length !== undefined) {this.physics.springLength = properties.length;}
+ this.canvasTopLeft = canvasTopLeft;
+ this.canvasBottomRight = canvasBottomRight;
+ };
- if (properties.color !== undefined) {
- this.options.inheritColor = false;
- if (util.isString(properties.color)) {
- this.options.color.color = properties.color;
- this.options.color.highlight = properties.color;
- }
- else {
- if (properties.color.color !== undefined) {this.options.color.color = properties.color.color;}
- if (properties.color.highlight !== undefined) {this.options.color.highlight = properties.color.highlight;}
- if (properties.color.hover !== undefined) {this.options.color.hover = properties.color.hover;}
- }
- }
- // A node is connected when it has a from and to node.
- this.connect();
+ /**
+ * This allows the zoom level of the network to influence the rendering
+ *
+ * @param scale
+ */
+ Node.prototype.setScale = function(scale) {
+ this.networkScaleInv = 1.0/scale;
+ this.networkScale = scale;
+ };
- this.widthFixed = this.widthFixed || (properties.width !== undefined);
- this.lengthFixed = this.lengthFixed || (properties.length !== undefined);
- this.widthSelected = this.options.width* this.options.widthSelectionMultiplier;
- // set draw method based on style
- switch (this.options.style) {
- case 'line': this.draw = this._drawLine; break;
- case 'arrow': this.draw = this._drawArrow; break;
- case 'arrow-center': this.draw = this._drawArrowCenter; break;
- case 'dash-line': this.draw = this._drawDashLine; break;
- default: this.draw = this._drawLine; break;
- }
+ /**
+ * set the velocity at 0. Is called when this node is contained in another during clustering
+ */
+ Node.prototype.clearVelocity = function() {
+ this.vx = 0;
+ this.vy = 0;
};
+
/**
- * Connect an edge to its nodes
+ * Basic preservation of (kinectic) energy
+ *
+ * @param massBeforeClustering
*/
- Edge.prototype.connect = function () {
- this.disconnect();
+ Node.prototype.updateVelocity = function(massBeforeClustering) {
+ var energyBefore = this.vx * this.vx * massBeforeClustering;
+ //this.vx = (this.vx < 0) ? -Math.sqrt(energyBefore/this.options.mass) : Math.sqrt(energyBefore/this.options.mass);
+ this.vx = Math.sqrt(energyBefore/this.options.mass);
+ energyBefore = this.vy * this.vy * massBeforeClustering;
+ //this.vy = (this.vy < 0) ? -Math.sqrt(energyBefore/this.options.mass) : Math.sqrt(energyBefore/this.options.mass);
+ this.vy = Math.sqrt(energyBefore/this.options.mass);
+ };
- this.from = this.network.nodes[this.fromId] || null;
- this.to = this.network.nodes[this.toId] || null;
- this.connected = (this.from && this.to);
+ module.exports = Node;
- if (this.connected) {
- this.from.attachEdge(this);
- this.to.attachEdge(this);
- }
- else {
- if (this.from) {
- this.from.detachEdge(this);
- }
- if (this.to) {
- this.to.detachEdge(this);
- }
- }
- };
+
+/***/ },
+/* 54 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var util = __webpack_require__(1);
/**
- * Disconnect an edge from its nodes
+ * @class Groups
+ * This class can store groups and properties specific for groups.
*/
- Edge.prototype.disconnect = function () {
- if (this.from) {
- this.from.detachEdge(this);
- this.from = null;
- }
- if (this.to) {
- this.to.detachEdge(this);
- this.to = null;
- }
+ function Groups() {
+ this.clear();
+ this.defaultIndex = 0;
+ }
- this.connected = false;
- };
/**
- * get the title of this edge.
- * @return {string} title The title of the edge, or undefined when no title
- * has been set.
+ * default constants for group colors
*/
- Edge.prototype.getTitle = function() {
- return typeof this.title === "function" ? this.title() : this.title;
- };
+ Groups.DEFAULT = [
+ {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}, hover: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
+ {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}, hover: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
+ {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}, hover: {border: "#FA0A10", background: "#FFAFB1"}}, // red
+ {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}, hover: {border: "#41A906", background: "#A1EC76"}}, // green
+ {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}, hover: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
+ {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}, hover: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
+ {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}, hover: {border: "#C37F00", background: "#FFCA66"}}, // orange
+ {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}, hover: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
+ {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}, hover: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
+ {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}, hover: {border: "#4AD63A", background: "#E6FFE3"}} // mint
+ ];
/**
- * Retrieve the value of the edge. Can be undefined
- * @return {Number} value
+ * Clear all groups
*/
- Edge.prototype.getValue = function() {
- return this.value;
+ Groups.prototype.clear = function () {
+ this.groups = {};
+ this.groups.length = function()
+ {
+ var i = 0;
+ for ( var p in this ) {
+ if (this.hasOwnProperty(p)) {
+ i++;
+ }
+ }
+ return i;
+ }
};
+
/**
- * Adjust the value range of the edge. The edge will adjust it's width
- * based on its value.
- * @param {Number} min
- * @param {Number} max
+ * get group properties of a groupname. If groupname is not found, a new group
+ * is added.
+ * @param {*} groupname Can be a number, string, Date, etc.
+ * @return {Object} group The created group, containing all group properties
*/
- Edge.prototype.setValueRange = function(min, max) {
- if (!this.widthFixed && this.value !== undefined) {
- var scale = (this.options.widthMax - this.options.widthMin) / (max - min);
- this.options.width= (this.value - min) * scale + this.options.widthMin;
- this.widthSelected = this.options.width* this.options.widthSelectionMultiplier;
+ Groups.prototype.get = function (groupname) {
+ var group = this.groups[groupname];
+ if (group == undefined) {
+ // create new group
+ var index = this.defaultIndex % Groups.DEFAULT.length;
+ this.defaultIndex++;
+ group = {};
+ group.color = Groups.DEFAULT[index];
+ this.groups[groupname] = group;
}
+
+ return group;
};
/**
- * Redraw a edge
- * Draw this edge in the given canvas
- * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
- * @param {CanvasRenderingContext2D} ctx
+ * Add a custom group style
+ * @param {String} groupname
+ * @param {Object} style An object containing borderColor,
+ * backgroundColor, etc.
+ * @return {Object} group The created group object
*/
- Edge.prototype.draw = function(ctx) {
- throw "Method draw not initialized in edge";
+ Groups.prototype.add = function (groupname, style) {
+ this.groups[groupname] = style;
+ return style;
};
+ module.exports = Groups;
+
+
+/***/ },
+/* 55 */
+/***/ function(module, exports, __webpack_require__) {
+
/**
- * Check if this object is overlapping with the provided object
- * @param {Object} obj an object with parameters left, top
- * @return {boolean} True if location is located on the edge
+ * @class Images
+ * This class loads images and keeps them stored.
*/
- Edge.prototype.isOverlappingWith = function(obj) {
- if (this.connected) {
- var distMax = 10;
- var xFrom = this.from.x;
- var yFrom = this.from.y;
- var xTo = this.to.x;
- var yTo = this.to.y;
- var xObj = obj.left;
- var yObj = obj.top;
+ function Images() {
+ this.images = {};
- var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj);
+ this.callback = undefined;
+ }
- return (dist < distMax);
- }
- else {
- return false
- }
+ /**
+ * Set an onload callback function. This will be called each time an image
+ * is loaded
+ * @param {function} callback
+ */
+ Images.prototype.setOnloadCallback = function(callback) {
+ this.callback = callback;
};
- Edge.prototype._getColor = function() {
- var colorObj = this.options.color;
- if (this.options.inheritColor == "to") {
- colorObj = {
- highlight: this.to.options.color.highlight.border,
- hover: this.to.options.color.hover.border,
- color: this.to.options.color.border
- };
- }
- else if (this.options.inheritColor == "from" || this.options.inheritColor == true) {
- colorObj = {
- highlight: this.from.options.color.highlight.border,
- hover: this.from.options.color.hover.border,
- color: this.from.options.color.border
+ /**
+ *
+ * @param {string} url Url of the image
+ * @param {string} url Url of an image to use if the url image is not found
+ * @return {Image} img The image object
+ */
+ Images.prototype.load = function(url, brokenUrl) {
+ var img = this.images[url];
+ if (img == undefined) {
+ // create the image
+ var images = this;
+ img = new Image();
+ this.images[url] = img;
+ img.onload = function() {
+ if (images.callback) {
+ images.callback(this);
+ }
};
+
+ img.onerror = function () {
+ this.src = brokenUrl;
+ if (images.callback) {
+ images.callback(this);
+ }
+ };
+
+ img.src = url;
}
- if (this.selected == true) {return colorObj.highlight;}
- else if (this.hover == true) {return colorObj.hover;}
- else {return colorObj.color;}
+ return img;
};
+ module.exports = Images;
- /**
- * Redraw a edge as a line
- * Draw this edge in the given canvas
- * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
- * @param {CanvasRenderingContext2D} ctx
- * @private
- */
- Edge.prototype._drawLine = function(ctx) {
- // set style
- ctx.strokeStyle = this._getColor();
- ctx.lineWidth = this._getLineWidth();
-
- if (this.from != this.to) {
- // draw line
- var via = this._line(ctx);
- // draw label
- var point;
- if (this.label) {
- if (this.options.smoothCurves.enabled == true && via != null) {
- var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
- var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
- point = {x:midpointX, y:midpointY};
- }
- else {
- point = this._pointOnLine(0.5);
- }
- this._label(ctx, this.label, point.x, point.y);
- }
- }
- else {
- var x, y;
- var radius = this.physics.springLength / 4;
- var node = this.from;
- if (!node.width) {
- node.resize(ctx);
- }
- if (node.width > node.height) {
- x = node.x + node.width / 2;
- y = node.y - radius;
- }
- else {
- x = node.x + radius;
- y = node.y - node.height / 2;
- }
- this._circle(ctx, x, y, radius);
- point = this._pointOnCircle(x, y, radius, 0.5);
- this._label(ctx, this.label, point.x, point.y);
- }
- };
+/***/ },
+/* 56 */
+/***/ function(module, exports, __webpack_require__) {
/**
- * Get the line width of the edge. Depends on width and whether one of the
- * connected nodes is selected.
- * @return {Number} width
- * @private
+ * Popup is a class to create a popup window with some text
+ * @param {Element} container The container object.
+ * @param {Number} [x]
+ * @param {Number} [y]
+ * @param {String} [text]
+ * @param {Object} [style] An object containing borderColor,
+ * backgroundColor, etc.
*/
- Edge.prototype._getLineWidth = function() {
- if (this.selected == true) {
- return Math.max(Math.min(this.widthSelected, this.options.widthMax), 0.3*this.networkScaleInv);
+ function Popup(container, x, y, text, style) {
+ if (container) {
+ this.container = container;
}
else {
- if (this.hover == true) {
- return Math.max(Math.min(this.options.hoverWidth, this.options.widthMax), 0.3*this.networkScaleInv);
- }
- else {
- return Math.max(this.options.width, 0.3*this.networkScaleInv);
- }
+ this.container = document.body;
}
- };
-
- Edge.prototype._getViaCoordinates = function () {
- var xVia = null;
- var yVia = null;
- var factor = this.options.smoothCurves.roundness;
- var type = this.options.smoothCurves.type;
- var dx = Math.abs(this.from.x - this.to.x);
- var dy = Math.abs(this.from.y - this.to.y);
- if (type == 'discrete' || type == 'diagonalCross') {
- if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) {
- if (this.from.y > this.to.y) {
- if (this.from.x < this.to.x) {
- xVia = this.from.x + factor * dy;
- yVia = this.from.y - factor * dy;
- }
- else if (this.from.x > this.to.x) {
- xVia = this.from.x - factor * dy;
- yVia = this.from.y - factor * dy;
- }
- }
- else if (this.from.y < this.to.y) {
- if (this.from.x < this.to.x) {
- xVia = this.from.x + factor * dy;
- yVia = this.from.y + factor * dy;
- }
- else if (this.from.x > this.to.x) {
- xVia = this.from.x - factor * dy;
- yVia = this.from.y + factor * dy;
- }
- }
- if (type == "discrete") {
- xVia = dx < factor * dy ? this.from.x : xVia;
- }
- }
- else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) {
- if (this.from.y > this.to.y) {
- if (this.from.x < this.to.x) {
- xVia = this.from.x + factor * dx;
- yVia = this.from.y - factor * dx;
- }
- else if (this.from.x > this.to.x) {
- xVia = this.from.x - factor * dx;
- yVia = this.from.y - factor * dx;
- }
- }
- else if (this.from.y < this.to.y) {
- if (this.from.x < this.to.x) {
- xVia = this.from.x + factor * dx;
- yVia = this.from.y + factor * dx;
- }
- else if (this.from.x > this.to.x) {
- xVia = this.from.x - factor * dx;
- yVia = this.from.y + factor * dx;
+ // x, y and text are optional, see if a style object was passed in their place
+ if (style === undefined) {
+ if (typeof x === "object") {
+ style = x;
+ x = undefined;
+ } else if (typeof text === "object") {
+ style = text;
+ text = undefined;
+ } else {
+ // for backwards compatibility, in case clients other than Network are creating Popup directly
+ style = {
+ fontColor: 'black',
+ fontSize: 14, // px
+ fontFace: 'verdana',
+ color: {
+ border: '#666',
+ background: '#FFFFC6'
}
}
- if (type == "discrete") {
- yVia = dy < factor * dx ? this.from.y : yVia;
- }
- }
- }
- else if (type == "straightCross") {
- if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { // up - down
- xVia = this.from.x;
- if (this.from.y < this.to.y) {
- yVia = this.to.y - (1-factor) * dy;
- }
- else {
- yVia = this.to.y + (1-factor) * dy;
- }
- }
- else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { // left - right
- if (this.from.x < this.to.x) {
- xVia = this.to.x - (1-factor) * dx;
- }
- else {
- xVia = this.to.x + (1-factor) * dx;
- }
- yVia = this.from.y;
- }
- }
- else if (type == 'horizontal') {
- if (this.from.x < this.to.x) {
- xVia = this.to.x - (1-factor) * dx;
- }
- else {
- xVia = this.to.x + (1-factor) * dx;
- }
- yVia = this.from.y;
- }
- else if (type == 'vertical') {
- xVia = this.from.x;
- if (this.from.y < this.to.y) {
- yVia = this.to.y - (1-factor) * dy;
- }
- else {
- yVia = this.to.y + (1-factor) * dy;
}
}
- else { // continuous
- if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) {
- if (this.from.y > this.to.y) {
- if (this.from.x < this.to.x) {
- // console.log(1)
- xVia = this.from.x + factor * dy;
- yVia = this.from.y - factor * dy;
- xVia = this.to.x < xVia ? this.to.x : xVia;
- }
- else if (this.from.x > this.to.x) {
- // console.log(2)
- xVia = this.from.x - factor * dy;
- yVia = this.from.y - factor * dy;
- xVia = this.to.x > xVia ? this.to.x :xVia;
- }
- }
- else if (this.from.y < this.to.y) {
- if (this.from.x < this.to.x) {
- // console.log(3)
- xVia = this.from.x + factor * dy;
- yVia = this.from.y + factor * dy;
- xVia = this.to.x < xVia ? this.to.x : xVia;
- }
- else if (this.from.x > this.to.x) {
- // console.log(4, this.from.x, this.to.x)
- xVia = this.from.x - factor * dy;
- yVia = this.from.y + factor * dy;
- xVia = this.to.x > xVia ? this.to.x : xVia;
- }
- }
- }
- else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) {
- if (this.from.y > this.to.y) {
- if (this.from.x < this.to.x) {
- // console.log(5)
- xVia = this.from.x + factor * dx;
- yVia = this.from.y - factor * dx;
- yVia = this.to.y > yVia ? this.to.y : yVia;
- }
- else if (this.from.x > this.to.x) {
- // console.log(6)
- xVia = this.from.x - factor * dx;
- yVia = this.from.y - factor * dx;
- yVia = this.to.y > yVia ? this.to.y : yVia;
- }
- }
- else if (this.from.y < this.to.y) {
- if (this.from.x < this.to.x) {
- // console.log(7)
- xVia = this.from.x + factor * dx;
- yVia = this.from.y + factor * dx;
- yVia = this.to.y < yVia ? this.to.y : yVia;
- }
- else if (this.from.x > this.to.x) {
- // console.log(8)
- xVia = this.from.x - factor * dx;
- yVia = this.from.y + factor * dx;
- yVia = this.to.y < yVia ? this.to.y : yVia;
- }
- }
- }
+
+ this.x = 0;
+ this.y = 0;
+ this.padding = 5;
+
+ if (x !== undefined && y !== undefined ) {
+ this.setPosition(x, y);
+ }
+ if (text !== undefined) {
+ this.setText(text);
}
+ // create the frame
+ this.frame = document.createElement("div");
+ var styleAttr = this.frame.style;
+ styleAttr.position = "absolute";
+ styleAttr.visibility = "hidden";
+ styleAttr.border = "1px solid " + style.color.border;
+ styleAttr.color = style.fontColor;
+ styleAttr.fontSize = style.fontSize + "px";
+ styleAttr.fontFamily = style.fontFace;
+ styleAttr.padding = this.padding + "px";
+ styleAttr.backgroundColor = style.color.background;
+ styleAttr.borderRadius = "3px";
+ styleAttr.MozBorderRadius = "3px";
+ styleAttr.WebkitBorderRadius = "3px";
+ styleAttr.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
+ styleAttr.whiteSpace = "nowrap";
+ this.container.appendChild(this.frame);
+ }
- return {x:xVia, y:yVia};
+ /**
+ * @param {number} x Horizontal position of the popup window
+ * @param {number} y Vertical position of the popup window
+ */
+ Popup.prototype.setPosition = function(x, y) {
+ this.x = parseInt(x);
+ this.y = parseInt(y);
};
/**
- * Draw a line between two nodes
- * @param {CanvasRenderingContext2D} ctx
- * @private
+ * Set the content for the popup window. This can be HTML code or text.
+ * @param {string | Element} content
*/
- Edge.prototype._line = function (ctx) {
- // draw a straight line
- ctx.beginPath();
- ctx.moveTo(this.from.x, this.from.y);
- if (this.options.smoothCurves.enabled == true) {
- if (this.options.smoothCurves.dynamic == false) {
- var via = this._getViaCoordinates();
- if (via.x == null) {
- ctx.lineTo(this.to.x, this.to.y);
- ctx.stroke();
- return null;
- }
- else {
- // this.via.x = via.x;
- // this.via.y = via.y;
- ctx.quadraticCurveTo(via.x,via.y,this.to.x, this.to.y);
- ctx.stroke();
- return via;
- }
- }
- else {
- ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
- ctx.stroke();
- return this.via;
- }
+ Popup.prototype.setText = function(content) {
+ if (content instanceof Element) {
+ this.frame.innerHTML = '';
+ this.frame.appendChild(content);
}
else {
- ctx.lineTo(this.to.x, this.to.y);
- ctx.stroke();
- return null;
+ this.frame.innerHTML = content; // string containing text or HTML
}
};
/**
- * Draw a line from a node to itself, a circle
- * @param {CanvasRenderingContext2D} ctx
- * @param {Number} x
- * @param {Number} y
- * @param {Number} radius
- * @private
- */
- Edge.prototype._circle = function (ctx, x, y, radius) {
- // draw a circle
- ctx.beginPath();
- ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
- ctx.stroke();
- };
-
- /**
- * Draw label with white background and with the middle at (x, y)
- * @param {CanvasRenderingContext2D} ctx
- * @param {String} text
- * @param {Number} x
- * @param {Number} y
- * @private
+ * Show the popup window
+ * @param {boolean} show Optional. Show or hide the window
*/
- Edge.prototype._label = function (ctx, text, x, y) {
- if (text) {
- ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
- this.options.fontSize + "px " + this.options.fontFace;
- var yLine;
-
- if (this.dirtyLabel == true) {
- var lines = String(text).split('\n');
- var lineCount = lines.length;
- var fontSize = (Number(this.options.fontSize) + 4);
- yLine = y + (1 - lineCount) / 2 * fontSize;
+ Popup.prototype.show = function (show) {
+ if (show === undefined) {
+ show = true;
+ }
- var width = ctx.measureText(lines[0]).width;
- for (var i = 1; i < lineCount; i++) {
- var lineWidth = ctx.measureText(lines[i]).width;
- width = lineWidth > width ? lineWidth : width;
- }
- var height = this.options.fontSize * lineCount;
- var left = x - width / 2;
- var top = y - height / 2;
+ if (show) {
+ var height = this.frame.clientHeight;
+ var width = this.frame.clientWidth;
+ var maxHeight = this.frame.parentNode.clientHeight;
+ var maxWidth = this.frame.parentNode.clientWidth;
- // cache
- this.labelDimensions = {top:top,left:left,width:width,height:height,yLine:yLine};
+ var top = (this.y - height);
+ if (top + height + this.padding > maxHeight) {
+ top = maxHeight - height - this.padding;
}
-
-
- if (this.options.fontFill !== undefined && this.options.fontFill !== null && this.options.fontFill !== "none") {
- ctx.fillStyle = this.options.fontFill;
- ctx.fillRect(this.labelDimensions.left,
- this.labelDimensions.top,
- this.labelDimensions.width,
- this.labelDimensions.height);
+ if (top < this.padding) {
+ top = this.padding;
}
- // draw text
- ctx.fillStyle = this.options.fontColor || "black";
- ctx.textAlign = "center";
- ctx.textBaseline = "middle";
- yLine = this.labelDimensions.yLine;
- for (var i = 0; i < lineCount; i++) {
- ctx.fillText(lines[i], x, yLine);
- yLine += fontSize;
+ var left = this.x;
+ if (left + width + this.padding > maxWidth) {
+ left = maxWidth - width - this.padding;
+ }
+ if (left < this.padding) {
+ left = this.padding;
}
+
+ this.frame.style.left = left + "px";
+ this.frame.style.top = top + "px";
+ this.frame.style.visibility = "visible";
+ }
+ else {
+ this.hide();
}
};
/**
- * Redraw a edge as a dashed line
- * Draw this edge in the given canvas
- * @author David Jordan
- * @date 2012-08-08
- * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
- * @param {CanvasRenderingContext2D} ctx
- * @private
+ * Hide the popup window
*/
- Edge.prototype._drawDashLine = function(ctx) {
- // set style
- ctx.strokeStyle = this._getColor();
- ctx.lineWidth = this._getLineWidth();
+ Popup.prototype.hide = function () {
+ this.frame.style.visibility = "hidden";
+ };
- var via = null;
- // only firefox and chrome support this method, else we use the legacy one.
- if (ctx.mozDash !== undefined || ctx.setLineDash !== undefined) {
- // configure the dash pattern
- var pattern = [0];
- if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) {
- pattern = [this.options.dash.length,this.options.dash.gap];
- }
- else {
- pattern = [5,5];
- }
+ module.exports = Popup;
- // set dash settings for chrome or firefox
- if (typeof ctx.setLineDash !== 'undefined') { //Chrome
- ctx.setLineDash(pattern);
- ctx.lineDashOffset = 0;
- } else { //Firefox
- ctx.mozDash = pattern;
- ctx.mozDashOffset = 0;
- }
+/***/ },
+/* 57 */
+/***/ function(module, exports, __webpack_require__) {
- // draw the line
- via = this._line(ctx);
+ /**
+ * Parse a text source containing data in DOT language into a JSON object.
+ * The object contains two lists: one with nodes and one with edges.
+ *
+ * DOT language reference: http://www.graphviz.org/doc/info/lang.html
+ *
+ * @param {String} data Text containing a graph in DOT-notation
+ * @return {Object} graph An object containing two parameters:
+ * {Object[]} nodes
+ * {Object[]} edges
+ */
+ function parseDOT (data) {
+ dot = data;
+ return parseGraph();
+ }
- // restore the dash settings.
- if (typeof ctx.setLineDash !== 'undefined') { //Chrome
- ctx.setLineDash([0]);
- ctx.lineDashOffset = 0;
+ // token types enumeration
+ var TOKENTYPE = {
+ NULL : 0,
+ DELIMITER : 1,
+ IDENTIFIER: 2,
+ UNKNOWN : 3
+ };
- } else { //Firefox
- ctx.mozDash = [0];
- ctx.mozDashOffset = 0;
- }
- }
- else { // unsupporting smooth lines
- // draw dashed line
- ctx.beginPath();
- ctx.lineCap = 'round';
- if (this.options.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value
- {
- ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
- [this.options.dash.length,this.options.dash.gap,this.options.dash.altLength,this.options.dash.gap]);
- }
- else if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) //If a dash and gap value has been set add to the array this value
- {
- ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
- [this.options.dash.length,this.options.dash.gap]);
- }
- else //If all else fails draw a line
- {
- ctx.moveTo(this.from.x, this.from.y);
- ctx.lineTo(this.to.x, this.to.y);
- }
- ctx.stroke();
- }
+ // map with all delimiters
+ var DELIMITERS = {
+ '{': true,
+ '}': true,
+ '[': true,
+ ']': true,
+ ';': true,
+ '=': true,
+ ',': true,
- // draw label
- if (this.label) {
- var point;
- if (this.options.smoothCurves.enabled == true && via != null) {
- var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
- var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
- point = {x:midpointX, y:midpointY};
- }
- else {
- point = this._pointOnLine(0.5);
- }
- this._label(ctx, this.label, point.x, point.y);
- }
+ '->': true,
+ '--': true
};
+ var dot = ''; // current dot file
+ var index = 0; // current index in dot file
+ var c = ''; // current token character in expr
+ var token = ''; // current token
+ var tokenType = TOKENTYPE.NULL; // type of the token
+
/**
- * Get a point on a line
- * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
- * @return {Object} point
- * @private
+ * Get the first character from the dot file.
+ * The character is stored into the char c. If the end of the dot file is
+ * reached, the function puts an empty string in c.
*/
- Edge.prototype._pointOnLine = function (percentage) {
- return {
- x: (1 - percentage) * this.from.x + percentage * this.to.x,
- y: (1 - percentage) * this.from.y + percentage * this.to.y
- }
- };
+ function first() {
+ index = 0;
+ c = dot.charAt(0);
+ }
/**
- * Get a point on a circle
- * @param {Number} x
- * @param {Number} y
- * @param {Number} radius
- * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
- * @return {Object} point
- * @private
+ * Get the next character from the dot file.
+ * The character is stored into the char c. If the end of the dot file is
+ * reached, the function puts an empty string in c.
*/
- Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
- var angle = (percentage - 3/8) * 2 * Math.PI;
- return {
- x: x + radius * Math.cos(angle),
- y: y - radius * Math.sin(angle)
- }
- };
+ function next() {
+ index++;
+ c = dot.charAt(index);
+ }
/**
- * Redraw a edge as a line with an arrow halfway the line
- * Draw this edge in the given canvas
- * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
- * @param {CanvasRenderingContext2D} ctx
- * @private
+ * Preview the next character from the dot file.
+ * @return {String} cNext
*/
- Edge.prototype._drawArrowCenter = function(ctx) {
- var point;
- // set style
- ctx.strokeStyle = this._getColor();
- ctx.fillStyle = ctx.strokeStyle;
- ctx.lineWidth = this._getLineWidth();
+ function nextPreview() {
+ return dot.charAt(index + 1);
+ }
- if (this.from != this.to) {
- // draw line
- var via = this._line(ctx);
+ /**
+ * Test whether given character is alphabetic or numeric
+ * @param {String} c
+ * @return {Boolean} isAlphaNumeric
+ */
+ var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
+ function isAlphaNumeric(c) {
+ return regexAlphaNumeric.test(c);
+ }
- var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
- var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
- // draw an arrow halfway the line
- if (this.options.smoothCurves.enabled == true && via != null) {
- var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
- var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
- point = {x:midpointX, y:midpointY};
+ /**
+ * Merge all properties of object b into object b
+ * @param {Object} a
+ * @param {Object} b
+ * @return {Object} a
+ */
+ function merge (a, b) {
+ if (!a) {
+ a = {};
+ }
+
+ if (b) {
+ for (var name in b) {
+ if (b.hasOwnProperty(name)) {
+ a[name] = b[name];
+ }
+ }
+ }
+ return a;
+ }
+
+ /**
+ * Set a value in an object, where the provided parameter name can be a
+ * path with nested parameters. For example:
+ *
+ * var obj = {a: 2};
+ * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
+ *
+ * @param {Object} obj
+ * @param {String} path A parameter name or dot-separated parameter path,
+ * like "color.highlight.border".
+ * @param {*} value
+ */
+ function setValue(obj, path, value) {
+ var keys = path.split('.');
+ var o = obj;
+ while (keys.length) {
+ var key = keys.shift();
+ if (keys.length) {
+ // this isn't the end point
+ if (!o[key]) {
+ o[key] = {};
+ }
+ o = o[key];
}
else {
- point = this._pointOnLine(0.5);
+ // this is the end point
+ o[key] = value;
}
+ }
+ }
- ctx.arrow(point.x, point.y, angle, length);
- ctx.fill();
- ctx.stroke();
+ /**
+ * Add a node to a graph object. If there is already a node with
+ * the same id, their attributes will be merged.
+ * @param {Object} graph
+ * @param {Object} node
+ */
+ function addNode(graph, node) {
+ var i, len;
+ var current = null;
- // draw label
- if (this.label) {
- this._label(ctx, this.label, point.x, point.y);
- }
+ // find root graph (in case of subgraph)
+ var graphs = [graph]; // list with all graphs from current graph to root graph
+ var root = graph;
+ while (root.parent) {
+ graphs.push(root.parent);
+ root = root.parent;
}
- else {
- // draw circle
- var x, y;
- var radius = 0.25 * Math.max(100,this.physics.springLength);
- var node = this.from;
- if (!node.width) {
- node.resize(ctx);
- }
- if (node.width > node.height) {
- x = node.x + node.width * 0.5;
- y = node.y - radius;
+
+ // find existing node (at root level) by its id
+ if (root.nodes) {
+ for (i = 0, len = root.nodes.length; i < len; i++) {
+ if (node.id === root.nodes[i].id) {
+ current = root.nodes[i];
+ break;
+ }
}
- else {
- x = node.x + radius;
- y = node.y - node.height * 0.5;
+ }
+
+ if (!current) {
+ // this is a new node
+ current = {
+ id: node.id
+ };
+ if (graph.node) {
+ // clone default attributes
+ current.attr = merge(current.attr, graph.node);
}
- this._circle(ctx, x, y, radius);
+ }
- // draw all arrows
- var angle = 0.2 * Math.PI;
- var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
- point = this._pointOnCircle(x, y, radius, 0.5);
- ctx.arrow(point.x, point.y, angle, length);
- ctx.fill();
- ctx.stroke();
+ // add node to this (sub)graph and all its parent graphs
+ for (i = graphs.length - 1; i >= 0; i--) {
+ var g = graphs[i];
- // draw label
- if (this.label) {
- point = this._pointOnCircle(x, y, radius, 0.5);
- this._label(ctx, this.label, point.x, point.y);
+ if (!g.nodes) {
+ g.nodes = [];
+ }
+ if (g.nodes.indexOf(current) == -1) {
+ g.nodes.push(current);
}
}
- };
-
+ // merge attributes
+ if (node.attr) {
+ current.attr = merge(current.attr, node.attr);
+ }
+ }
/**
- * Redraw a edge as a line with an arrow
- * Draw this edge in the given canvas
- * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
- * @param {CanvasRenderingContext2D} ctx
- * @private
+ * Add an edge to a graph object
+ * @param {Object} graph
+ * @param {Object} edge
*/
- Edge.prototype._drawArrow = function(ctx) {
- // set style
- ctx.strokeStyle = this._getColor();
- ctx.fillStyle = ctx.strokeStyle;
- ctx.lineWidth = this._getLineWidth();
-
- var angle, length;
- //draw a line
- if (this.from != this.to) {
- angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
- var dx = (this.to.x - this.from.x);
- var dy = (this.to.y - this.from.y);
- var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
+ function addEdge(graph, edge) {
+ if (!graph.edges) {
+ graph.edges = [];
+ }
+ graph.edges.push(edge);
+ if (graph.edge) {
+ var attr = merge({}, graph.edge); // clone default attributes
+ edge.attr = merge(attr, edge.attr); // merge attributes
+ }
+ }
- var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
- var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
- var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
- var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
+ /**
+ * Create an edge to a graph object
+ * @param {Object} graph
+ * @param {String | Number | Object} from
+ * @param {String | Number | Object} to
+ * @param {String} type
+ * @param {Object | null} attr
+ * @return {Object} edge
+ */
+ function createEdge(graph, from, to, type, attr) {
+ var edge = {
+ from: from,
+ to: to,
+ type: type
+ };
- var via;
- if (this.options.smoothCurves.dynamic == true && this.options.smoothCurves.enabled == true ) {
- via = this.via;
- }
- else if (this.options.smoothCurves.enabled == true) {
- via = this._getViaCoordinates();
- }
+ if (graph.edge) {
+ edge.attr = merge({}, graph.edge); // clone default attributes
+ }
+ edge.attr = merge(edge.attr || {}, attr); // merge attributes
- if (this.options.smoothCurves.enabled == true && via.x != null) {
- angle = Math.atan2((this.to.y - via.y), (this.to.x - via.x));
- dx = (this.to.x - via.x);
- dy = (this.to.y - via.y);
- edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
- }
- var toBorderDist = this.to.distanceToBorder(ctx, angle);
- var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
+ return edge;
+ }
- var xTo,yTo;
- if (this.options.smoothCurves.enabled == true && via.x != null) {
- xTo = (1 - toBorderPoint) * via.x + toBorderPoint * this.to.x;
- yTo = (1 - toBorderPoint) * via.y + toBorderPoint * this.to.y;
- }
- else {
- xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
- yTo = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
- }
+ /**
+ * Get next token in the current dot file.
+ * The token and token type are available as token and tokenType
+ */
+ function getToken() {
+ tokenType = TOKENTYPE.NULL;
+ token = '';
- ctx.beginPath();
- ctx.moveTo(xFrom,yFrom);
- if (this.options.smoothCurves.enabled == true && via.x != null) {
- ctx.quadraticCurveTo(via.x,via.y,xTo, yTo);
- }
- else {
- ctx.lineTo(xTo, yTo);
- }
- ctx.stroke();
+ // skip over whitespaces
+ while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
+ next();
+ }
- // draw arrow at the end of the line
- length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
- ctx.arrow(xTo, yTo, angle, length);
- ctx.fill();
- ctx.stroke();
+ do {
+ var isComment = false;
- // draw label
- if (this.label) {
- var point;
- if (this.options.smoothCurves.enabled == true && via != null) {
- var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
- var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
- point = {x:midpointX, y:midpointY};
+ // skip comment
+ if (c == '#') {
+ // find the previous non-space character
+ var i = index - 1;
+ while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') {
+ i--;
}
- else {
- point = this._pointOnLine(0.5);
+ if (dot.charAt(i) == '\n' || dot.charAt(i) == '') {
+ // the # is at the start of a line, this is indeed a line comment
+ while (c != '' && c != '\n') {
+ next();
+ }
+ isComment = true;
}
- this._label(ctx, this.label, point.x, point.y);
}
- }
- else {
- // draw circle
- var node = this.from;
- var x, y, arrow;
- var radius = 0.25 * Math.max(100,this.physics.springLength);
- if (!node.width) {
- node.resize(ctx);
+ if (c == '/' && nextPreview() == '/') {
+ // skip line comment
+ while (c != '' && c != '\n') {
+ next();
+ }
+ isComment = true;
}
- if (node.width > node.height) {
- x = node.x + node.width * 0.5;
- y = node.y - radius;
- arrow = {
- x: x,
- y: node.y,
- angle: 0.9 * Math.PI
- };
+ if (c == '/' && nextPreview() == '*') {
+ // skip block comment
+ while (c != '') {
+ if (c == '*' && nextPreview() == '/') {
+ // end of block comment found. skip these last two characters
+ next();
+ next();
+ break;
+ }
+ else {
+ next();
+ }
+ }
+ isComment = true;
}
- else {
- x = node.x + radius;
- y = node.y - node.height * 0.5;
- arrow = {
- x: node.x,
- y: y,
- angle: 0.6 * Math.PI
- };
+
+ // skip over whitespaces
+ while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
+ next();
}
- ctx.beginPath();
- // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
- ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
- ctx.stroke();
+ }
+ while (isComment);
- // draw all arrows
- var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
- ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
- ctx.fill();
- ctx.stroke();
+ // check for end of dot file
+ if (c == '') {
+ // token is still empty
+ tokenType = TOKENTYPE.DELIMITER;
+ return;
+ }
- // draw label
- if (this.label) {
- point = this._pointOnCircle(x, y, radius, 0.5);
- this._label(ctx, this.label, point.x, point.y);
- }
+ // check for delimiters consisting of 2 characters
+ var c2 = c + nextPreview();
+ if (DELIMITERS[c2]) {
+ tokenType = TOKENTYPE.DELIMITER;
+ token = c2;
+ next();
+ next();
+ return;
}
- };
+ // check for delimiters consisting of 1 character
+ if (DELIMITERS[c]) {
+ tokenType = TOKENTYPE.DELIMITER;
+ token = c;
+ next();
+ return;
+ }
+ // check for an identifier (number or string)
+ // TODO: more precise parsing of numbers/strings (and the port separator ':')
+ if (isAlphaNumeric(c) || c == '-') {
+ token += c;
+ next();
- /**
- * Calculate the distance between a point (x3,y3) and a line segment from
- * (x1,y1) to (x2,y2).
- * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
- * @param {number} x1
- * @param {number} y1
- * @param {number} x2
- * @param {number} y2
- * @param {number} x3
- * @param {number} y3
- * @private
- */
- Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
- var returnValue = 0;
- if (this.from != this.to) {
- if (this.options.smoothCurves.enabled == true) {
- var xVia, yVia;
- if (this.options.smoothCurves.enabled == true && this.options.smoothCurves.dynamic == true) {
- xVia = this.via.x;
- yVia = this.via.y;
- }
- else {
- var via = this._getViaCoordinates();
- xVia = via.x;
- yVia = via.y;
- }
- var minDistance = 1e9;
- var distance;
- var i,t,x,y, lastX, lastY;
- for (i = 0; i < 10; i++) {
- t = 0.1*i;
- x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*xVia + Math.pow(t,2)*x2;
- y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*yVia + Math.pow(t,2)*y2;
- if (i > 0) {
- distance = this._getDistanceToLine(lastX,lastY,x,y, x3,y3);
- minDistance = distance < minDistance ? distance : minDistance;
- }
- lastX = x; lastY = y;
- }
- returnValue = minDistance;
+ while (isAlphaNumeric(c)) {
+ token += c;
+ next();
}
- else {
- returnValue = this._getDistanceToLine(x1,y1,x2,y2,x3,y3);
+ if (token == 'false') {
+ token = false; // convert to boolean
+ }
+ else if (token == 'true') {
+ token = true; // convert to boolean
+ }
+ else if (!isNaN(Number(token))) {
+ token = Number(token); // convert to number
}
+ tokenType = TOKENTYPE.IDENTIFIER;
+ return;
}
- else {
- var x, y, dx, dy;
- var radius = 0.25 * this.physics.springLength;
- var node = this.from;
- if (node.width > node.height) {
- x = node.x + 0.5 * node.width;
- y = node.y - radius;
+
+ // check for a string enclosed by double quotes
+ if (c == '"') {
+ next();
+ while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
+ token += c;
+ if (c == '"') { // skip the escape character
+ next();
+ }
+ next();
}
- else {
- x = node.x + radius;
- y = node.y - 0.5 * node.height;
+ if (c != '"') {
+ throw newSyntaxError('End of string " expected');
}
- dx = x - x3;
- dy = y - y3;
- returnValue = Math.abs(Math.sqrt(dx*dx + dy*dy) - radius);
+ next();
+ tokenType = TOKENTYPE.IDENTIFIER;
+ return;
}
- if (this.labelDimensions.left < x3 &&
- this.labelDimensions.left + this.labelDimensions.width > x3 &&
- this.labelDimensions.top < y3 &&
- this.labelDimensions.top + this.labelDimensions.height > y3) {
- return 0;
- }
- else {
- return returnValue;
+ // something unknown is found, wrong characters, a syntax error
+ tokenType = TOKENTYPE.UNKNOWN;
+ while (c != '') {
+ token += c;
+ next();
}
- };
+ throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
+ }
- Edge.prototype._getDistanceToLine = function(x1,y1,x2,y2,x3,y3) {
- var px = x2-x1,
- py = y2-y1,
- something = px*px + py*py,
- u = ((x3 - x1) * px + (y3 - y1) * py) / something;
+ /**
+ * Parse a graph.
+ * @returns {Object} graph
+ */
+ function parseGraph() {
+ var graph = {};
- if (u > 1) {
- u = 1;
+ first();
+ getToken();
+
+ // optional strict keyword
+ if (token == 'strict') {
+ graph.strict = true;
+ getToken();
}
- else if (u < 0) {
- u = 0;
+
+ // graph or digraph keyword
+ if (token == 'graph' || token == 'digraph') {
+ graph.type = token;
+ getToken();
}
- var x = x1 + u * px,
- y = y1 + u * py,
- dx = x - x3,
- dy = y - y3;
+ // optional graph id
+ if (tokenType == TOKENTYPE.IDENTIFIER) {
+ graph.id = token;
+ getToken();
+ }
- //# Note: If the actual distance does not matter,
- //# if you only want to compare what this function
- //# returns to other results of this function, you
- //# can just return the squared distance instead
- //# (i.e. remove the sqrt) to gain a little performance
+ // open angle bracket
+ if (token != '{') {
+ throw newSyntaxError('Angle bracket { expected');
+ }
+ getToken();
- return Math.sqrt(dx*dx + dy*dy);
- };
+ // statements
+ parseStatements(graph);
- /**
- * This allows the zoom level of the network to influence the rendering
- *
- * @param scale
- */
- Edge.prototype.setScale = function(scale) {
- this.networkScaleInv = 1.0/scale;
- };
+ // close angle bracket
+ if (token != '}') {
+ throw newSyntaxError('Angle bracket } expected');
+ }
+ getToken();
+ // end of file
+ if (token !== '') {
+ throw newSyntaxError('End of file expected');
+ }
+ getToken();
- Edge.prototype.select = function() {
- this.selected = true;
- };
+ // remove temporary default properties
+ delete graph.node;
+ delete graph.edge;
+ delete graph.graph;
- Edge.prototype.unselect = function() {
- this.selected = false;
- };
+ return graph;
+ }
- Edge.prototype.positionBezierNode = function() {
- if (this.via !== null && this.from !== null && this.to !== null) {
- this.via.x = 0.5 * (this.from.x + this.to.x);
- this.via.y = 0.5 * (this.from.y + this.to.y);
- }
- else {
- this.via.x = 0;
- this.via.y = 0;
+ /**
+ * Parse a list with statements.
+ * @param {Object} graph
+ */
+ function parseStatements (graph) {
+ while (token !== '' && token != '}') {
+ parseStatement(graph);
+ if (token == ';') {
+ getToken();
+ }
}
- };
+ }
/**
- * This function draws the control nodes for the manipulator.
- * In order to enable this, only set the this.controlNodesEnabled to true.
- * @param ctx
+ * Parse a single statement. Can be a an attribute statement, node
+ * statement, a series of node statements and edge statements, or a
+ * parameter.
+ * @param {Object} graph
*/
- Edge.prototype._drawControlNodes = function(ctx) {
- if (this.controlNodesEnabled == true) {
- if (this.controlNodes.from === null && this.controlNodes.to === null) {
- var nodeIdFrom = "edgeIdFrom:".concat(this.id);
- var nodeIdTo = "edgeIdTo:".concat(this.id);
- var constants = {
- nodes:{group:'', radius:8},
- physics:{damping:0},
- clustering: {maxNodeSizeIncrements: 0 ,nodeScaling: {width:0, height: 0, radius:0}}
- };
- this.controlNodes.from = new Node(
- {id:nodeIdFrom,
- shape:'dot',
- color:{background:'#ff4e00', border:'#3c3c3c', highlight: {background:'#07f968'}}
- },{},{},constants);
- this.controlNodes.to = new Node(
- {id:nodeIdTo,
- shape:'dot',
- color:{background:'#ff4e00', border:'#3c3c3c', highlight: {background:'#07f968'}}
- },{},{},constants);
- }
+ function parseStatement(graph) {
+ // parse subgraph
+ var subgraph = parseSubgraph(graph);
+ if (subgraph) {
+ // edge statements
+ parseEdge(graph, subgraph);
- if (this.controlNodes.from.selected == false && this.controlNodes.to.selected == false) {
- this.controlNodes.positions = this.getControlNodePositions(ctx);
- this.controlNodes.from.x = this.controlNodes.positions.from.x;
- this.controlNodes.from.y = this.controlNodes.positions.from.y;
- this.controlNodes.to.x = this.controlNodes.positions.to.x;
- this.controlNodes.to.y = this.controlNodes.positions.to.y;
- }
+ return;
+ }
- this.controlNodes.from.draw(ctx);
- this.controlNodes.to.draw(ctx);
+ // parse an attribute statement
+ var attr = parseAttributeStatement(graph);
+ if (attr) {
+ return;
+ }
+
+ // parse node
+ if (tokenType != TOKENTYPE.IDENTIFIER) {
+ throw newSyntaxError('Identifier expected');
+ }
+ var id = token; // id can be a string or a number
+ getToken();
+
+ if (token == '=') {
+ // id statement
+ getToken();
+ if (tokenType != TOKENTYPE.IDENTIFIER) {
+ throw newSyntaxError('Identifier expected');
+ }
+ graph[id] = token;
+ getToken();
+ // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
}
else {
- this.controlNodes = {from:null, to:null, positions:{}};
+ parseNodeStatement(graph, id);
}
- };
+ }
/**
- * Enable control nodes.
- * @private
+ * Parse a subgraph
+ * @param {Object} graph parent graph object
+ * @return {Object | null} subgraph
*/
- Edge.prototype._enableControlNodes = function() {
- this.fromBackup = this.from;
- this.toBackup = this.to;
- this.controlNodesEnabled = true;
- };
+ function parseSubgraph (graph) {
+ var subgraph = null;
- /**
- * disable control nodes and remove from dynamicEdges from old node
- * @private
- */
- Edge.prototype._disableControlNodes = function() {
- this.fromId = this.from.id;
- this.toId = this.to.id;
- if (this.fromId != this.fromBackup.id) { // from was changed, remove edge from old 'from' node dynamic edges
- this.fromBackup.detachEdge(this);
- }
- else if (this.toId != this.toBackup.id) { // to was changed, remove edge from old 'to' node dynamic edges
- this.toBackup.detachEdge(this);
+ // optional subgraph keyword
+ if (token == 'subgraph') {
+ subgraph = {};
+ subgraph.type = 'subgraph';
+ getToken();
+
+ // optional graph id
+ if (tokenType == TOKENTYPE.IDENTIFIER) {
+ subgraph.id = token;
+ getToken();
+ }
}
- this.fromBackup = null;
- this.toBackup = null;
- this.controlNodesEnabled = false;
- };
+ // open angle bracket
+ if (token == '{') {
+ getToken();
+
+ if (!subgraph) {
+ subgraph = {};
+ }
+ subgraph.parent = graph;
+ subgraph.node = graph.node;
+ subgraph.edge = graph.edge;
+ subgraph.graph = graph.graph;
+
+ // statements
+ parseStatements(subgraph);
+
+ // close angle bracket
+ if (token != '}') {
+ throw newSyntaxError('Angle bracket } expected');
+ }
+ getToken();
+
+ // remove temporary default properties
+ delete subgraph.node;
+ delete subgraph.edge;
+ delete subgraph.graph;
+ delete subgraph.parent;
+
+ // register at the parent graph
+ if (!graph.subgraphs) {
+ graph.subgraphs = [];
+ }
+ graph.subgraphs.push(subgraph);
+ }
+ return subgraph;
+ }
/**
- * This checks if one of the control nodes is selected and if so, returns the control node object. Else it returns null.
- * @param x
- * @param y
- * @returns {null}
- * @private
+ * parse an attribute statement like "node [shape=circle fontSize=16]".
+ * Available keywords are 'node', 'edge', 'graph'.
+ * The previous list with default attributes will be replaced
+ * @param {Object} graph
+ * @returns {String | null} keyword Returns the name of the parsed attribute
+ * (node, edge, graph), or null if nothing
+ * is parsed.
*/
- Edge.prototype._getSelectedControlNode = function(x,y) {
- var positions = this.controlNodes.positions;
- var fromDistance = Math.sqrt(Math.pow(x - positions.from.x,2) + Math.pow(y - positions.from.y,2));
- var toDistance = Math.sqrt(Math.pow(x - positions.to.x ,2) + Math.pow(y - positions.to.y ,2));
+ function parseAttributeStatement (graph) {
+ // attribute statements
+ if (token == 'node') {
+ getToken();
- if (fromDistance < 15) {
- this.connectedNode = this.from;
- this.from = this.controlNodes.from;
- return this.controlNodes.from;
+ // node attributes
+ graph.node = parseAttributeList();
+ return 'node';
}
- else if (toDistance < 15) {
- this.connectedNode = this.to;
- this.to = this.controlNodes.to;
- return this.controlNodes.to;
+ else if (token == 'edge') {
+ getToken();
+
+ // edge attributes
+ graph.edge = parseAttributeList();
+ return 'edge';
}
- else {
- return null;
+ else if (token == 'graph') {
+ getToken();
+
+ // graph attributes
+ graph.graph = parseAttributeList();
+ return 'graph';
}
- };
+ return null;
+ }
/**
- * this resets the control nodes to their original position.
- * @private
+ * parse a node statement
+ * @param {Object} graph
+ * @param {String | Number} id
*/
- Edge.prototype._restoreControlNodes = function() {
- if (this.controlNodes.from.selected == true) {
- this.from = this.connectedNode;
- this.connectedNode = null;
- this.controlNodes.from.unselect();
- }
- else if (this.controlNodes.to.selected == true) {
- this.to = this.connectedNode;
- this.connectedNode = null;
- this.controlNodes.to.unselect();
+ function parseNodeStatement(graph, id) {
+ // node statement
+ var node = {
+ id: id
+ };
+ var attr = parseAttributeList();
+ if (attr) {
+ node.attr = attr;
}
- };
+ addNode(graph, node);
+
+ // edge statements
+ parseEdge(graph, id);
+ }
/**
- * this calculates the position of the control nodes on the edges of the parent nodes.
- *
- * @param ctx
- * @returns {{from: {x: number, y: number}, to: {x: *, y: *}}}
+ * Parse an edge or a series of edges
+ * @param {Object} graph
+ * @param {String | Number} from Id of the from node
*/
- Edge.prototype.getControlNodePositions = function(ctx) {
- var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
- var dx = (this.to.x - this.from.x);
- var dy = (this.to.y - this.from.y);
- var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
- var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
- var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
- var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
- var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
-
- var via;
- if (this.options.smoothCurves.dynamic == true && this.options.smoothCurves.enabled == true) {
- via = this.via;
- }
- else if (this.options.smoothCurves.enabled == true) {
- via = this._getViaCoordinates();
- }
-
- if (this.options.smoothCurves.enabled == true && via.x != null) {
- angle = Math.atan2((this.to.y - via.y), (this.to.x - via.x));
- dx = (this.to.x - via.x);
- dy = (this.to.y - via.y);
- edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
- }
- var toBorderDist = this.to.distanceToBorder(ctx, angle);
- var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
+ function parseEdge(graph, from) {
+ while (token == '->' || token == '--') {
+ var to;
+ var type = token;
+ getToken();
- var xTo,yTo;
- if (this.options.smoothCurves.enabled == true && via.x != null) {
- xTo = (1 - toBorderPoint) * via.x + toBorderPoint * this.to.x;
- yTo = (1 - toBorderPoint) * via.y + toBorderPoint * this.to.y;
- }
- else {
- xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
- yTo = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
- }
+ var subgraph = parseSubgraph(graph);
+ if (subgraph) {
+ to = subgraph;
+ }
+ else {
+ if (tokenType != TOKENTYPE.IDENTIFIER) {
+ throw newSyntaxError('Identifier or subgraph expected');
+ }
+ to = token;
+ addNode(graph, {
+ id: to
+ });
+ getToken();
+ }
- return {from:{x:xFrom,y:yFrom},to:{x:xTo,y:yTo}};
- };
+ // parse edge attributes
+ var attr = parseAttributeList();
- module.exports = Edge;
+ // create edge
+ var edge = createEdge(graph, from, to, type, attr);
+ addEdge(graph, edge);
-/***/ },
-/* 58 */
-/***/ function(module, exports, __webpack_require__) {
+ from = to;
+ }
+ }
/**
- * Popup is a class to create a popup window with some text
- * @param {Element} container The container object.
- * @param {Number} [x]
- * @param {Number} [y]
- * @param {String} [text]
- * @param {Object} [style] An object containing borderColor,
- * backgroundColor, etc.
+ * Parse a set with attributes,
+ * for example [label="1.000", shape=solid]
+ * @return {Object | null} attr
*/
- function Popup(container, x, y, text, style) {
- if (container) {
- this.container = container;
- }
- else {
- this.container = document.body;
- }
+ function parseAttributeList() {
+ var attr = null;
- // x, y and text are optional, see if a style object was passed in their place
- if (style === undefined) {
- if (typeof x === "object") {
- style = x;
- x = undefined;
- } else if (typeof text === "object") {
- style = text;
- text = undefined;
- } else {
- // for backwards compatibility, in case clients other than Network are creating Popup directly
- style = {
- fontColor: 'black',
- fontSize: 14, // px
- fontFace: 'verdana',
- color: {
- border: '#666',
- background: '#FFFFC6'
- }
+ while (token == '[') {
+ getToken();
+ attr = {};
+ while (token !== '' && token != ']') {
+ if (tokenType != TOKENTYPE.IDENTIFIER) {
+ throw newSyntaxError('Attribute name expected');
}
- }
- }
+ var name = token;
- this.x = 0;
- this.y = 0;
- this.padding = 5;
+ getToken();
+ if (token != '=') {
+ throw newSyntaxError('Equal sign = expected');
+ }
+ getToken();
- if (x !== undefined && y !== undefined ) {
- this.setPosition(x, y);
- }
- if (text !== undefined) {
- this.setText(text);
- }
+ if (tokenType != TOKENTYPE.IDENTIFIER) {
+ throw newSyntaxError('Attribute value expected');
+ }
+ var value = token;
+ setValue(attr, name, value); // name can be a path
- // create the frame
- this.frame = document.createElement("div");
- var styleAttr = this.frame.style;
- styleAttr.position = "absolute";
- styleAttr.visibility = "hidden";
- styleAttr.border = "1px solid " + style.color.border;
- styleAttr.color = style.fontColor;
- styleAttr.fontSize = style.fontSize + "px";
- styleAttr.fontFamily = style.fontFace;
- styleAttr.padding = this.padding + "px";
- styleAttr.backgroundColor = style.color.background;
- styleAttr.borderRadius = "3px";
- styleAttr.MozBorderRadius = "3px";
- styleAttr.WebkitBorderRadius = "3px";
- styleAttr.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
- styleAttr.whiteSpace = "nowrap";
- this.container.appendChild(this.frame);
+ getToken();
+ if (token ==',') {
+ getToken();
+ }
+ }
+
+ if (token != ']') {
+ throw newSyntaxError('Bracket ] expected');
+ }
+ getToken();
+ }
+
+ return attr;
}
/**
- * @param {number} x Horizontal position of the popup window
- * @param {number} y Vertical position of the popup window
+ * Create a syntax error with extra information on current token and index.
+ * @param {String} message
+ * @returns {SyntaxError} err
*/
- Popup.prototype.setPosition = function(x, y) {
- this.x = parseInt(x);
- this.y = parseInt(y);
- };
+ function newSyntaxError(message) {
+ return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
+ }
/**
- * Set the content for the popup window. This can be HTML code or text.
- * @param {string | Element} content
+ * Chop off text after a maximum length
+ * @param {String} text
+ * @param {Number} maxLength
+ * @returns {String}
*/
- Popup.prototype.setText = function(content) {
- if (content instanceof Element) {
- this.frame.innerHTML = '';
- this.frame.appendChild(content);
+ function chop (text, maxLength) {
+ return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
+ }
+
+ /**
+ * Execute a function fn for each pair of elements in two arrays
+ * @param {Array | *} array1
+ * @param {Array | *} array2
+ * @param {function} fn
+ */
+ function forEach2(array1, array2, fn) {
+ if (Array.isArray(array1)) {
+ array1.forEach(function (elem1) {
+ if (Array.isArray(array2)) {
+ array2.forEach(function (elem2) {
+ fn(elem1, elem2);
+ });
+ }
+ else {
+ fn(elem1, array2);
+ }
+ });
}
else {
- this.frame.innerHTML = content; // string containing text or HTML
+ if (Array.isArray(array2)) {
+ array2.forEach(function (elem2) {
+ fn(array1, elem2);
+ });
+ }
+ else {
+ fn(array1, array2);
+ }
}
- };
+ }
/**
- * Show the popup window
- * @param {boolean} show Optional. Show or hide the window
+ * Convert a string containing a graph in DOT language into a map containing
+ * with nodes and edges in the format of graph.
+ * @param {String} data Text containing a graph in DOT-notation
+ * @return {Object} graphData
*/
- Popup.prototype.show = function (show) {
- if (show === undefined) {
- show = true;
- }
+ function DOTToGraph (data) {
+ // parse the DOT file
+ var dotData = parseDOT(data);
+ var graphData = {
+ nodes: [],
+ edges: [],
+ options: {}
+ };
- if (show) {
- var height = this.frame.clientHeight;
- var width = this.frame.clientWidth;
- var maxHeight = this.frame.parentNode.clientHeight;
- var maxWidth = this.frame.parentNode.clientWidth;
+ // copy the nodes
+ if (dotData.nodes) {
+ dotData.nodes.forEach(function (dotNode) {
+ var graphNode = {
+ id: dotNode.id,
+ label: String(dotNode.label || dotNode.id)
+ };
+ merge(graphNode, dotNode.attr);
+ if (graphNode.image) {
+ graphNode.shape = 'image';
+ }
+ graphData.nodes.push(graphNode);
+ });
+ }
- var top = (this.y - height);
- if (top + height + this.padding > maxHeight) {
- top = maxHeight - height - this.padding;
- }
- if (top < this.padding) {
- top = this.padding;
+ // copy the edges
+ if (dotData.edges) {
+ /**
+ * Convert an edge in DOT format to an edge with VisGraph format
+ * @param {Object} dotEdge
+ * @returns {Object} graphEdge
+ */
+ var convertEdge = function (dotEdge) {
+ var graphEdge = {
+ from: dotEdge.from,
+ to: dotEdge.to
+ };
+ merge(graphEdge, dotEdge.attr);
+ graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
+ return graphEdge;
}
- var left = this.x;
- if (left + width + this.padding > maxWidth) {
- left = maxWidth - width - this.padding;
- }
- if (left < this.padding) {
- left = this.padding;
+ dotData.edges.forEach(function (dotEdge) {
+ var from, to;
+ if (dotEdge.from instanceof Object) {
+ from = dotEdge.from.nodes;
+ }
+ else {
+ from = {
+ id: dotEdge.from
+ }
+ }
+
+ if (dotEdge.to instanceof Object) {
+ to = dotEdge.to.nodes;
+ }
+ else {
+ to = {
+ id: dotEdge.to
+ }
+ }
+
+ if (dotEdge.from instanceof Object && dotEdge.from.edges) {
+ dotEdge.from.edges.forEach(function (subEdge) {
+ var graphEdge = convertEdge(subEdge);
+ graphData.edges.push(graphEdge);
+ });
+ }
+
+ forEach2(from, to, function (from, to) {
+ var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
+ var graphEdge = convertEdge(subEdge);
+ graphData.edges.push(graphEdge);
+ });
+
+ if (dotEdge.to instanceof Object && dotEdge.to.edges) {
+ dotEdge.to.edges.forEach(function (subEdge) {
+ var graphEdge = convertEdge(subEdge);
+ graphData.edges.push(graphEdge);
+ });
+ }
+ });
+ }
+
+ // copy the options
+ if (dotData.attr) {
+ graphData.options = dotData.attr;
+ }
+
+ return graphData;
+ }
+
+ // exports
+ exports.parseDOT = parseDOT;
+ exports.DOTToGraph = DOTToGraph;
+
+
+/***/ },
+/* 58 */
+/***/ function(module, exports, __webpack_require__) {
+
+
+ function parseGephi(gephiJSON, options) {
+ var edges = [];
+ var nodes = [];
+ this.options = {
+ edges: {
+ inheritColor: true
+ },
+ nodes: {
+ allowedToMove: false,
+ parseColor: false
}
+ };
- this.frame.style.left = left + "px";
- this.frame.style.top = top + "px";
- this.frame.style.visibility = "visible";
+ if (options !== undefined) {
+ this.options.nodes['allowedToMove'] = options.allowedToMove | false;
+ this.options.nodes['parseColor'] = options.parseColor | false;
+ this.options.edges['inheritColor'] = options.inheritColor | true;
}
- else {
- this.hide();
+
+ var gEdges = gephiJSON.edges;
+ var gNodes = gephiJSON.nodes;
+ for (var i = 0; i < gEdges.length; i++) {
+ var edge = {};
+ var gEdge = gEdges[i];
+ edge['id'] = gEdge.id;
+ edge['from'] = gEdge.source;
+ edge['to'] = gEdge.target;
+ edge['attributes'] = gEdge.attributes;
+ // edge['value'] = gEdge.attributes !== undefined ? gEdge.attributes.Weight : undefined;
+ // edge['width'] = edge['value'] !== undefined ? undefined : edgegEdge.size;
+ edge['color'] = gEdge.color;
+ edge['inheritColor'] = edge['color'] !== undefined ? false : this.options.inheritColor;
+ edges.push(edge);
}
- };
- /**
- * Hide the popup window
- */
- Popup.prototype.hide = function () {
- this.frame.style.visibility = "hidden";
- };
+ for (var i = 0; i < gNodes.length; i++) {
+ var node = {};
+ var gNode = gNodes[i];
+ node['id'] = gNode.id;
+ node['attributes'] = gNode.attributes;
+ node['x'] = gNode.x;
+ node['y'] = gNode.y;
+ node['label'] = gNode.label;
+ if (this.options.nodes.parseColor == true) {
+ node['color'] = gNode.color;
+ }
+ else {
+ node['color'] = gNode.color !== undefined ? {background:gNode.color, border:gNode.color} : undefined;
+ }
+ node['radius'] = gNode.size;
+ node['allowedToMoveX'] = this.options.nodes.allowedToMove;
+ node['allowedToMoveY'] = this.options.nodes.allowedToMove;
+ nodes.push(node);
+ }
- module.exports = Popup;
+ return {nodes:nodes, edges:edges};
+ }
+ exports.parseGephi = parseGephi;
/***/ },
/* 59 */
@@ -31304,7 +31309,7 @@ return /******/ (function(modules) { // webpackBootstrap
/***/ function(module, exports, __webpack_require__) {
var util = __webpack_require__(1);
- var Node = __webpack_require__(56);
+ var Node = __webpack_require__(53);
/**
* Creation of the SectorMixin var.
@@ -31862,7 +31867,7 @@ return /******/ (function(modules) { // webpackBootstrap
/* 66 */
/***/ function(module, exports, __webpack_require__) {
- var Node = __webpack_require__(56);
+ var Node = __webpack_require__(53);
/**
* This function can be called from the _doInAllSectors function
@@ -32577,8 +32582,8 @@ return /******/ (function(modules) { // webpackBootstrap
/***/ function(module, exports, __webpack_require__) {
var util = __webpack_require__(1);
- var Node = __webpack_require__(56);
- var Edge = __webpack_require__(57);
+ var Node = __webpack_require__(53);
+ var Edge = __webpack_require__(52);
/**
* clears the toolbar div element of children
diff --git a/examples/network/03_images.html b/examples/network/03_images.html
index 7f3776f1..3d858116 100644
--- a/examples/network/03_images.html
+++ b/examples/network/03_images.html
@@ -34,10 +34,10 @@
// Create a data table with links.
edges = [];
- nodes.push({id: 1, label: 'Main', image: DIR + 'Network-Pipe-icon.png', shape: 'image'});
+ nodes.push({id: 1, label: 'Main', image: DIR + 'Network-Pipe-icon.png', shape: 'image'});
nodes.push({id: 2, label: 'Office', image: DIR + 'Network-Pipe-icon.png', shape: 'image'});
nodes.push({id: 3, label: 'Wireless', image: DIR + 'Network-Pipe-icon.png', shape: 'image'});
- edges.push({from: 1, to: 2, length: LENGTH_MAIN});
+ edges.push({from: 1, to: 2, title:'world', length: LENGTH_MAIN});
edges.push({from: 1, to: 3, length: LENGTH_MAIN});
for (var i = 4; i <= 7; i++) {
diff --git a/lib/network/Network.js b/lib/network/Network.js
index 0772ca0f..4be868f2 100644
--- a/lib/network/Network.js
+++ b/lib/network/Network.js
@@ -1301,6 +1301,7 @@ Network.prototype._checkShowPopup = function (pointer) {
var id;
var lastPopupNode = this.popupObj;
+ var nodeUnderCursor = false;
if (this.popupObj == undefined) {
// search the nodes for overlap, select the top one in case of multiple nodes
@@ -1308,15 +1309,19 @@ Network.prototype._checkShowPopup = function (pointer) {
for (id in nodes) {
if (nodes.hasOwnProperty(id)) {
var node = nodes[id];
- if (node.getTitle() !== undefined && node.isOverlappingWith(obj)) {
- this.popupObj = node;
- break;
+ if (node.isOverlappingWith(obj)) {
+ if (node.getTitle() !== undefined) {
+ this.popupObj = node;
+ break;
+ }
+ // if you hover over a node, the title of the edge is not supposed to be shown.
+ nodeUnderCursor = true;
}
}
}
}
- if (this.popupObj === undefined) {
+ if (this.popupObj === undefined && nodeUnderCursor == false) {
// search the edges for overlap
var edges = this.edges;
for (id in edges) {