diff --git a/.gitignore b/.gitignore index a182852b..0cfb188a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ node_modules .project .settings/ npm-debug.log +.directory # vim temporary files .*.sw[op] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a6660fb3..414327c7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,21 +1,14 @@ ## Contributing -Contributions to the vis.js library are very welcome! We can't do this alone. - -You can contribute in different ways: spread the word, report bugs, come up with -ideas and suggestions, and contribute to the code. - -If you have any **general question** on how to use the vis.js library in your -project please check out -[stackoverflow](http://stackoverflow.com/questions/tagged/vis.js) for that. - -There are a few preferences regarding **code contributions**: - -- vis.js follows the node.js code style as described - [here](http://nodeguide.com/style.html). -- When implementing new features, please update the documentation accordingly. -- Send pull requests to the `develop` branch, not the `master` branch. -- Only commit changes done in the source files under `lib`, not to the builds - which are located in the folder `dist`. - -Thanks! +[Contributions](//github.com/almende/vis/blob/master/misc/how_to_help.md) to the vis.js library are very welcome! [We can't do this alone](//github.com/almende/vis/blob/master/misc/we_need_help.md). + +### Questions +If you have any *general question* on how to use the vis.js library in your own project please check out [stackoverflow](http://stackoverflow.com/questions/tagged/vis.js) for thinks like that. **This is NOT a JavaScript help forum!** + +### Bugs, Problems and Feature-Requests +If you really want to open a new issue: +* Please use the [search functionality](//github.com/almende/vis/issues) to make sure that there is not already an issue concerning the same topic. +* Please make sure to **mention which module** of vis.js (network, timeline, graph3d, ...) your are referring to. +* If you think you found a bug please **provide a simple example** (e.g. on [jsbin](jsbin.com)) that demonstrates the problem. +* If you want to propose a feature-request please **describe what you are looking for in detail**, ideally providing a screenshot, drawing or something similar. +* **Close the issue later**, when the issue is no longer needed. diff --git a/docs/graph3d/index.html b/docs/graph3d/index.html index 16467484..429e2a98 100644 --- a/docs/graph3d/index.html +++ b/docs/graph3d/index.html @@ -474,7 +474,8 @@ var options = { The contents of the tooltip can be customized by providing a callback function as tooltip. In this case the function is called with an object containing parameters x, - y, and z argument, + y, z, and data + (the source JS object for the point) as an argument, and must return a string which may contain HTML. diff --git a/docs/network/edges.html b/docs/network/edges.html index 42c132ac..423f9a45 100644 --- a/docs/network/edges.html +++ b/docs/network/edges.html @@ -115,9 +115,9 @@ var options = { edges:{ arrows: { - to: {enabled: false, scaleFactor:1}, - middle: {enabled: false, scaleFactor:1}, - from: {enabled: false, scaleFactor:1} + to: {enabled: false, scaleFactor:1, type:'arrow'}, + middle: {enabled: false, scaleFactor:1, type:'arrow'}, + from: {enabled: false, scaleFactor:1, type:'arrow'} }, arrowStrikethrough: true, color: { @@ -243,6 +243,12 @@ network.setOptions(options); 1 The scale factor allows you to change the size of the arrowhead. + + arrows.to.type + String + arrow + The type of endpoint. Default is arrow. Also possible is circle. + arrows.middle Object or Boolean @@ -703,4 +709,4 @@ var options: { - \ No newline at end of file + diff --git a/docs/network/index.html b/docs/network/index.html index 0f8ef8ff..cfd03d64 100644 --- a/docs/network/index.html +++ b/docs/network/index.html @@ -328,19 +328,23 @@ network.setOptions(options); locale String 'en' - Select the locale. By default, the language is English. If you want to use another language, you - will - need to define your own locale and refer to it here. + Select the locale. By default, the language is English. locales Object defaultLocales - Locales object. By default only 'en', 'de', 'es' and 'nl' are supported. Take a look - at - the locales - section below for more explaination on how to customize this. + Locales object. By default + 'en', + 'de', + 'es', + 'it', + 'nl' + 'pt-br', + 'ru' + are supported. Take a look at the + locales section below + for more explaination on how to customize this. clickToUse @@ -998,7 +1002,7 @@ function releaseFunction (clusterPosition, containedNodesPositions) { setSelection( - Object selection, + Object selection, [Object options]) @@ -1237,7 +1241,13 @@ var options = { node any way you want. This is also the style object that is provided in the processProperties function for - fine tuning. If undefined, default node options will be used. + fine tuning. If undefined, default node options will be used.

+ Default functionality only allows clustering if the cluster will contain 2 or more nodes. To allow clustering of single nodes you can use the allowSingleNodeCluster : true property. +
+    clusterNodeProperties: {
+        allowSingleNodeCluster: true
+    }
+
clusterEdgeProperties diff --git a/docs/timeline/index.html b/docs/timeline/index.html index f11d1c89..cd02f3d2 100644 --- a/docs/timeline/index.html +++ b/docs/timeline/index.html @@ -31,13 +31,13 @@ - + - + - + @@ -213,7 +213,7 @@ or a DataView (offering 1 way data binding). Items are regular objects and can contain the properties start, end (optional), content, - group (optional), className (optional), + group (optional), className (optional), editable (optional), and style (optional).

@@ -557,7 +557,7 @@ function (option, path) { format - Object + Object or Function none Apply custom date formatting of the labels on the time axis. The default value of format is: @@ -583,8 +583,16 @@ function (option, path) { year: '' } } + For values which not provided in the customized options.format, the default values will be used. All available formatting syntax is described in the docs of moment.js. +
+ You can also use a function format for each label. The function accepts as arguments the date, scale and step in that order, and expects to return a string for the label. + +
function format({
+  minorLabels: Function(date: Date, scale: Number, step: Number),
+  majorLabels: Function(date: Date, scale: Number, step: Number)
+}
@@ -624,7 +632,7 @@ function (option, path) { If no order properties are provided, the order will be undetermined. - + groupOrderSwap Function @@ -640,7 +648,7 @@ function (option, path) { groupTemplate function none - A template function used to generate the contents of the groups. The function is called by the Timeline with a groups data as argument, and must return HTML code as result. When the option groupTemplate is specified, the groups do not need to have a field content. See section Templates for a detailed explanation. + A template function used to generate the contents of the groups. The function is called by the Timeline with a groups data as the first argument and the group element as the second, and must return HTML code as result. When the option groupTemplate is specified, the groups do not need to have a field content. See section Templates for a detailed explanation. @@ -787,7 +795,7 @@ function (option, path) { Only applicable when option selectable is true. - + multiselectPerGroup boolean @@ -805,7 +813,7 @@ function (option, path) { Callback function triggered when an item is about to be added: when the user double taps an empty space in the Timeline. See section Editing Items for more information. Only applicable when both options selectable and editable.add are set true. - + onAddGroup function @@ -829,7 +837,7 @@ function (option, path) { Callback function triggered when an item has been moved: after the user has dragged the item to an other position. See section Editing Items for more information. Only applicable when both options selectable and editable.updateTime or editable.updateGroup are set true. - + onMoveGroup function @@ -853,7 +861,7 @@ function (option, path) { Callback function triggered when an item is about to be removed: when the user tapped the delete button on the top right of a selected item. See section Editing Items for more information. Only applicable when both options selectable and editable.remove are set true. - + onRemoveGroup function @@ -970,14 +978,7 @@ function (option, path) { template function none - A template function used to generate the contents of the items. The function is called by the Timeline with an items data as argument, and must return HTML code as result. When the option template is specified, the items do not need to have a field content. See section Templates for a detailed explanation. - - - - throttleRedraw - number - 0 - Limit the maximum number of redraws to once every x milliseconds. For example setting throttleRedraw to `100` milliseconds will limit the number of redraws to 10 times per second. + A template function used to generate the contents of the items. The function is called by the Timeline with an items data as the first argument and the item element as the second, and must return HTML code as result. When the option template is specified, the items do not need to have a field content. See section Templates for a detailed explanation. @@ -1629,6 +1630,17 @@ var template = Handlebars.compile(source); }; +

React templates

+ +You can use a React component for the templates by rendering them to the templates' element directly: + +
+  template: function (item, element) {
+    return ReactDOM.render(<b>{item.content}</b>, element);
+  },
+
+ +

Multiple templates

In order to support multiple templates, the template handler can be extended to switch between different templates, depending on a specific item property: @@ -1703,6 +1715,14 @@ var options = { en_US + + Italian + + it
+ it_IT
+ it_CH + + Dutch diff --git a/examples/graph3d/11_tooltips.html b/examples/graph3d/11_tooltips.html index 508e7e0a..39af19aa 100644 --- a/examples/graph3d/11_tooltips.html +++ b/examples/graph3d/11_tooltips.html @@ -24,6 +24,11 @@ // Create and populate a data table. data = new vis.DataSet(); + var extra_content = [ + 'Arbitrary information', + 'You can access data from the point source object', + 'Tooltip example content', + ]; // create some nice looking data with sin/cos var steps = 5; // number of datapoints will be steps*steps @@ -34,10 +39,10 @@ var z = custom(x,y); if (withValue) { var value = (y - x); - data.add({x:x, y:y, z: z, style:value}); + data.add({x:x, y:y, z: z, style:value, extra: extra_content[(x*y) % extra_content.length]}); } else { - data.add({x:x, y:y, z: z}); + data.add({x:x, y:y, z: z, extra: extra_content[(x*y) % extra_content.length]}); } } } @@ -55,8 +60,9 @@ // Option tooltip can be true, false, or a function returning a string with HTML contents //tooltip: true, tooltip: function (point) { - // parameter point contains properties x, y, z - return 'value: ' + point.z + ''; + // parameter point contains properties x, y, z, and data + // data is the original object passed to the point constructor + return 'value: ' + point.z + '
' + point.data.extra; }, keepAspectRatio: true, diff --git a/examples/network/edgeStyles/arrowTypes.html b/examples/network/edgeStyles/arrowTypes.html new file mode 100644 index 00000000..25cf63bf --- /dev/null +++ b/examples/network/edgeStyles/arrowTypes.html @@ -0,0 +1,55 @@ + + + + Network | Basic usage + + + + + + + + +

+ There two type of liner endings. The classical "arrow" (default) and "circle". +

+ +
+ + + + + + diff --git a/examples/network/other/clustersOfclusters.html b/examples/network/other/clustersOfclusters.html new file mode 100644 index 00000000..0e90bcf0 --- /dev/null +++ b/examples/network/other/clustersOfclusters.html @@ -0,0 +1,75 @@ + + + + + Cluster Test + + + + + +

+ Clusters can contain other clusters, but clusters of a single node is only possible by adding +

allowSingleNodeCluster: true
+to clusterNodeProperties
+In this example repeatedly clicking on the node with open the Clusters. +

+
+
+ + + diff --git a/examples/network/other/manipulation.html b/examples/network/other/manipulation.html index c97dacb3..b399c098 100644 --- a/examples/network/other/manipulation.html +++ b/examples/network/other/manipulation.html @@ -1,6 +1,7 @@ + Network | Manipulation + + + + + + + + + +

+ In this example, the network data can be exported to JSON and imported back into the network. + + Try this out by exporting the network to JSON, clearing the network and then importing it again. The nodes will all appear in the same position as they were before the network was destroyed. +

+ +
+ +
+ + + + +
+ + + + \ No newline at end of file diff --git a/examples/timeline/groups/verticalItemsHide.html b/examples/timeline/groups/verticalItemsHide.html index 493a6ac7..1f38fffe 100644 --- a/examples/timeline/groups/verticalItemsHide.html +++ b/examples/timeline/groups/verticalItemsHide.html @@ -72,6 +72,7 @@ // create items var items = new vis.DataSet(); + var types = [ 'box', 'point', 'range', 'background'] var order = 1; var truck = 1; for (var j = 0; j < 25; j++) { @@ -83,11 +84,14 @@ date.setHours(date.getHours() + 2 + Math.floor(Math.random()*4)); var end = new Date(date); + var type = types[Math.floor(4 * Math.random())] + items.add({ id: order, group: truck, start: start, end: end, + type: type, content: 'Order ' + order }); diff --git a/examples/timeline/other/functionLabelFormats.html b/examples/timeline/other/functionLabelFormats.html new file mode 100644 index 00000000..9de9023b --- /dev/null +++ b/examples/timeline/other/functionLabelFormats.html @@ -0,0 +1,141 @@ + + + + Timeline | Custom function label format example + + + + + + + + + + + +

+ This example demonstrate using custom function label formats. +

+
+ + + + \ No newline at end of file diff --git a/examples/timeline/other/localization.html b/examples/timeline/other/localization.html index 4a05abad..ee1beeea 100644 --- a/examples/timeline/other/localization.html +++ b/examples/timeline/other/localization.html @@ -24,6 +24,7 @@

diff --git a/index-timeline-graph2d.js b/index-timeline-graph2d.js index 50426671..da9e5173 100644 --- a/index-timeline-graph2d.js +++ b/index-timeline-graph2d.js @@ -44,4 +44,4 @@ exports.timeline = { // bundled external libraries exports.moment = require('./lib/module/moment'); exports.Hammer = require('./lib/module/hammer'); -exports.keycharm = require('keycharm'); \ No newline at end of file +exports.keycharm = require('keycharm'); diff --git a/lib/DataView.js b/lib/DataView.js index 32a7325a..ba7a864e 100644 --- a/lib/DataView.js +++ b/lib/DataView.js @@ -300,6 +300,7 @@ DataView.prototype._onEvent = function (event, params, senderId) { var ids = params && params.items; var data = this._data; var updatedData = []; + var oldData = []; var added = []; var updated = []; var removed = []; @@ -330,6 +331,7 @@ DataView.prototype._onEvent = function (event, params, senderId) { if (this._ids[id]) { updated.push(id); updatedData.push(params.data[i]); + oldData.push(params.oldData[i]); } else { this._ids[id] = true; @@ -368,7 +370,7 @@ DataView.prototype._onEvent = function (event, params, senderId) { this._trigger('add', {items: added}, senderId); } if (updated.length) { - this._trigger('update', {items: updated, data: updatedData}, senderId); + this._trigger('update', {items: updated, oldData: oldData, data: updatedData}, senderId); } if (removed.length) { this._trigger('remove', {items: removed}, senderId); diff --git a/lib/graph3d/Filter.js b/lib/graph3d/Filter.js index 2315104c..41d734a3 100644 --- a/lib/graph3d/Filter.js +++ b/lib/graph3d/Filter.js @@ -111,7 +111,7 @@ Filter.prototype.getValues = function() { */ Filter.prototype.getValue = function(index) { if (index >= this.values.length) - throw 'Error: index out of range'; + throw new Error('Index out of range'); return this.values[index]; }; @@ -164,7 +164,7 @@ Filter.prototype.setOnLoadCallback = function(callback) { */ Filter.prototype.selectValue = function(index) { if (index >= this.values.length) - throw 'Error: index out of range'; + throw new Error('Index out of range'); this.index = index; this.value = this.values[index]; diff --git a/lib/graph3d/Graph3d.js b/lib/graph3d/Graph3d.js index f2c17507..102312a9 100644 --- a/lib/graph3d/Graph3d.js +++ b/lib/graph3d/Graph3d.js @@ -1,5 +1,4 @@ -var Emitter = require('emitter-component'); -var DataSet = require('../DataSet'); +var Emitter = require('emitter-component'); var DataSet = require('../DataSet'); var DataView = require('../DataView'); var util = require('../util'); var Point3d = require('./Point3d'); @@ -9,6 +8,245 @@ var Filter = require('./Filter'); var Slider = require('./Slider'); var StepNumber = require('./StepNumber'); +// ----------------------------------------------------------------------------- +// Definitions private to module +// ----------------------------------------------------------------------------- + +/// enumerate the available styles +Graph3d.STYLE = { + BAR : 0, + BARCOLOR: 1, + BARSIZE : 2, + DOT : 3, + DOTLINE : 4, + DOTCOLOR: 5, + DOTSIZE : 6, + GRID : 7, + LINE : 8, + SURFACE : 9 +}; + + +/** + * Field names in the options hash which are of relevance to the user. + * + * Specifically, these are the fields which require no special handling, + * and can be directly copied over. + */ +var OPTIONKEYS = [ + 'width', + 'height', + 'filterLabel', + 'legendLabel', + 'xLabel', + 'yLabel', + 'zLabel', + 'xValueLabel', + 'yValueLabel', + 'zValueLabel', + 'showGrid', + 'showPerspective', + 'showShadow', + 'keepAspectRatio', + 'verticalRatio', + 'showAnimationControls', + 'animationInterval', + 'animationPreload', + 'animationAutoStart', + 'axisColor', + 'gridColor', + 'xCenter', + 'yCenter' +]; + + +/** + * Field names in the options hash which are of relevance to the user. + * + * Same as OPTIONKEYS, but internally these fields are stored with + * prefix 'default' in the name. + */ +var PREFIXEDOPTIONKEYS = [ + 'xBarWidth', + 'yBarWidth', + 'valueMin', + 'valueMax', + 'xMin', + 'xMax', + 'xStep', + 'yMin', + 'yMax', + 'yStep', + 'zMin', + 'zMax', + 'zStep' +]; + + +/** + * Default values for option settings. + * + * These are the values used when a Graph3d instance is initialized + * without custom settings. + * + * If a field is not in this list, a default value of 'undefined' can + * be assumed. Of course, it does no harm to set a field explicitly to + * 'undefined' here. + * + * A value of 'undefined' here normally means: + * + * 'derive from current data and graph style' + * + * In the code, this is indicated by the comment 'auto by default'. + */ +var DEFAULTS = { + width : '400px', + height : '400px', + filterLabel : 'time', + legendLabel : 'value', + xLabel : 'x', + yLabel : 'y', + zLabel : 'z', + xValueLabel : function(v) { return v; }, + yValueLabel : function(v) { return v; }, + zValueLabel : function(v) { return v; }, + showGrid : true, + showPerspective : true, + showShadow : false, + keepAspectRatio : true, + verticalRatio : 0.5, // 0.1 to 1.0, where 1.0 results in a 'cube' + + showAnimationControls: undefined, // auto by default + animationInterval : 1000, // milliseconds + animationPreload : false, + animationAutoStart : undefined, // auto by default + + axisColor : '#4D4D4D', + gridColor : '#D3D3D3', + xCenter : '55%', + yCenter : '50%', + + // Following require special handling, therefore not mentioned in the OPTIONKEYS tables. + + style : Graph3d.STYLE.DOT, + tooltip : false, + showLegend : undefined, // auto by default (based on graph style) + backgroundColor : undefined, + + dataColor : { + fill : '#7DC1FF', + stroke : '#3267D2', + strokeWidth: 1 // px + }, + + cameraPosition : { + horizontal: 1.0, + vertical : 0.5, + distance : 1.7 + }, + + // Following stored internally with field prefix 'default' + // All these are 'auto by default' + + xBarWidth : undefined, + yBarWidth : undefined, + valueMin : undefined, + valueMax : undefined, + xMin : undefined, + xMax : undefined, + xStep : undefined, + yMin : undefined, + yMax : undefined, + yStep : undefined, + zMin : undefined, + zMax : undefined, + zStep : undefined +}; + + +/** + * Make first letter of parameter upper case. + * + * Source: http://stackoverflow.com/a/1026087 + */ +function capitalize(str) { + if (str === undefined || str === "") { + return str; + } + + return str.charAt(0).toUpperCase() + str.slice(1); +} + + +/** + * Add a prefix to a field name, taking style guide into account + */ +function prefixFieldName(prefix, fieldName) { + if (prefix === undefined || prefix === "") { + return fieldName; + } + + return prefix + capitalize(fieldName); +} + + +/** + * Forcibly copy fields from src to dst in a controlled manner. + * + * A given field in dst will always be overwitten. If this field + * is undefined or not present in src, the field in dst will + * be explicitly set to undefined. + * + * The intention here is to be able to reset all option fields. + * + * Only the fields mentioned in array 'fields' will be handled. + * + * @param fields array with names of fields to copy + * @param prefix optional; prefix to use for the target fields. + */ +function forceCopy(src, dst, fields, prefix) { + var srcKey; + var dstKey; + + for (var i in fields) { + srcKey = fields[i]; + dstKey = prefixFieldName(prefix, srcKey); + + dst[dstKey] = src[srcKey]; + } +} + + +/** + * Copy fields from src to dst in a safe and controlled manner. + * + * Only the fields mentioned in array 'fields' will be copied over, + * and only if these are actually defined. + * + * @param fields array with names of fields to copy + * @param prefix optional; prefix to use for the target fields. + */ +function safeCopy(src, dst, fields, prefix) { + var srcKey; + var dstKey; + + for (var i in fields) { + srcKey = fields[i]; + if (src[srcKey] === undefined) continue; + + dstKey = prefixFieldName(prefix, srcKey); + + dst[dstKey] = src[srcKey]; + } +} + + + +// ----------------------------------------------------------------------------- +// Class Graph3d +// ----------------------------------------------------------------------------- + + /** * @constructor Graph3d * Graph3d displays data in 3d. @@ -27,44 +265,34 @@ function Graph3d(container, data, options) { // create variables and set default values this.containerElement = container; - this.width = '400px'; - this.height = '400px'; - this.margin = 10; // px - this.defaultXCenter = '55%'; - this.defaultYCenter = '50%'; - - this.xLabel = 'x'; - this.yLabel = 'y'; - this.zLabel = 'z'; - - var passValueFn = function(v) { return v; }; - this.xValueLabel = passValueFn; - this.yValueLabel = passValueFn; - this.zValueLabel = passValueFn; - - this.filterLabel = 'time'; - this.legendLabel = 'value'; - this.showLegend = undefined; // auto by default (based on graph style) - - this.style = Graph3d.STYLE.DOT; - this.showPerspective = true; - this.showGrid = true; - this.keepAspectRatio = true; - this.showShadow = false; - this.showGrayBottom = false; // TODO: this does not work correctly - this.showTooltip = false; - this.verticalRatio = 0.5; // 0.1 to 1.0, where 1.0 results in a 'cube' - this.animationInterval = 1000; // milliseconds - this.animationPreload = false; + this.dataTable = null; // The original data table + this.dataPoints = null; // The table with point objects - this.camera = new Camera(); - this.camera.setArmRotation(1.0, 0.5); - this.camera.setArmLength(1.7); + // create a frame and canvas + this.create(); + + // + // Set Defaults + // + + // Handle the defaults which can be simply copied over + forceCopy(DEFAULTS, this, OPTIONKEYS); + forceCopy(DEFAULTS, this, PREFIXEDOPTIONKEYS, 'default'); + + // Following are internal fields, not part of the user settings + this.margin = 10; // px + this.showGrayBottom = false; // TODO: this does not work correctly + this.showTooltip = false; + this.dotSizeRatio = 0.02; // size of the dots as a fraction of the graph width this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window? - this.dataTable = null; // The original data table - this.dataPoints = null; // The table with point objects + // Handle the more complex ('special') fields + this._setSpecialSettings(DEFAULTS, this); + + // + // End Set Defaults + // // the column indexes this.colX = undefined; @@ -73,35 +301,8 @@ function Graph3d(container, data, options) { this.colValue = undefined; this.colFilter = undefined; - this.xMin = 0; - this.xStep = undefined; // auto by default - this.xMax = 1; - this.yMin = 0; - this.yStep = undefined; // auto by default - this.yMax = 1; - this.zMin = 0; - this.zStep = undefined; // auto by default - this.zMax = 1; - this.valueMin = 0; - this.valueMax = 1; - this.xBarWidth = 1; - this.yBarWidth = 1; // TODO: customize axis range - // colors - this.axisColor = '#4D4D4D'; - this.gridColor = '#D3D3D3'; - this.dataColor = { - fill: '#7DC1FF', - stroke: '#3267D2', - strokeWidth: 1 // px - }; - - this.dotSizeRatio = 0.02; // size of the dots as a fraction of the graph width - - // create a frame and canvas - this.create(); - // apply options (also when undefined) this.setOptions(options); @@ -223,15 +424,135 @@ Graph3d.prototype._convertTranslationToScreen = function(translation) { // shift and scale the point to the center of the screen // use the width of the graph to scale both horizontally and vertically. return new Point2d( - this.xcenter + bx * this.frame.canvas.clientWidth, - this.ycenter - by * this.frame.canvas.clientWidth); + this.currentXCenter + bx * this.frame.canvas.clientWidth, + this.currentYCenter - by * this.frame.canvas.clientWidth); }; + +/** + * Calculate the translations and screen positions of all points + */ +Graph3d.prototype._calcTranslations = function(points, sort) { + if (sort === undefined) { + sort = true; + } + + for (var i = 0; i < points.length; i++) { + var point = points[i]; + point.trans = this._convertPointToTranslation(point.point); + point.screen = this._convertTranslationToScreen(point.trans); + + // calculate the translation of the point at the bottom (needed for sorting) + var transBottom = this._convertPointToTranslation(point.bottom); + point.dist = this.showPerspective ? transBottom.length() : -transBottom.z; + } + + if (!sort) { + return; + } + + // sort the points on depth of their (x,y) position (not on z) + var sortDepth = function (a, b) { + return b.dist - a.dist; + }; + points.sort(sortDepth); +}; + + +// ----------------------------------------------------------------------------- +// Methods for handling settings +// ----------------------------------------------------------------------------- + + +/** + * Special handling for certain parameters + * + * 'Special' here means: setting requires more than a simple copy + */ +Graph3d.prototype._setSpecialSettings = function(src, dst) { + if (src.backgroundColor !== undefined) { + this._setBackgroundColor(src.backgroundColor, dst); + } + + this._setDataColor(src.dataColor, dst); + this._setStyle(src.style, dst); + this._setShowLegend(src.showLegend, dst); + this._setCameraPosition(src.cameraPosition, dst); + + // As special fields go, this is an easy one; just a translation of the name. + // Can't use this.tooltip directly, because that field exists internally + if (src.tooltip !== undefined) { + dst.showTooltip = src.tooltip; + } +}; + + +/** + * Set the value of setting 'showLegend' + * + * This depends on the value of the style fields, so it must be called + * after the style field has been initialized. + */ +Graph3d.prototype._setShowLegend = function(showLegend, dst) { + if (showLegend === undefined) { + // If the default was auto, make a choice for this field + var isAutoByDefault = (DEFAULTS.showLegend === undefined); + + if (isAutoByDefault) { + // these styles default to having legends + var isLegendGraphStyle = this.style === Graph3d.STYLE.DOTCOLOR + || this.style === Graph3d.STYLE.DOTSIZE; + + this.showLegend = isLegendGraphStyle; + } else { + // Leave current value as is + } + } else { + dst.showLegend = showLegend; + } +}; + + +Graph3d.prototype._setStyle = function(style, dst) { + if (style === undefined) { + return; // Nothing to do + } + + var styleNumber; + + if (typeof style === 'string') { + styleNumber = this._getStyleNumber(style); + + if (styleNumber === -1 ) { + throw new Error('Style \'' + style + '\' is invalid'); + } + } else { + // Do a pedantic check on style number value + var valid = false; + for (var n in Graph3d.STYLE) { + if (Graph3d.STYLE[n] === style) { + valid = true; + break; + } + } + + if (!valid) { + throw new Error('Style \'' + style + '\' is invalid'); + } + + styleNumber = style; + } + + dst.style = styleNumber; +}; + + + /** * Set the background styling for the graph * @param {string | {fill: string, stroke: string, strokeWidth: string}} backgroundColor */ -Graph3d.prototype._setBackgroundColor = function(backgroundColor) { +Graph3d.prototype._setBackgroundColor = function(backgroundColor, dst) { var fill = 'white'; var stroke = 'gray'; var strokeWidth = 1; @@ -246,34 +567,92 @@ Graph3d.prototype._setBackgroundColor = function(backgroundColor) { if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke; if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth; } - else if (backgroundColor === undefined) { - // use use defaults + else { + throw new Error('Unsupported type of backgroundColor'); + } + + dst.frame.style.backgroundColor = fill; + dst.frame.style.borderColor = stroke; + dst.frame.style.borderWidth = strokeWidth + 'px'; + dst.frame.style.borderStyle = 'solid'; +}; + + +Graph3d.prototype._setDataColor = function(dataColor, dst) { + if (dataColor === undefined) { + return; // Nothing to do + } + + if (dst.dataColor === undefined) { + dst.dataColor = {}; + } + + if (typeof dataColor === 'string') { + dst.dataColor.fill = dataColor; + dst.dataColor.stroke = dataColor; } else { - throw 'Unsupported type of backgroundColor'; + if (dataColor.fill) { + dst.dataColor.fill = dataColor.fill; + } + if (dataColor.stroke) { + dst.dataColor.stroke = dataColor.stroke; + } + if (dataColor.strokeWidth !== undefined) { + dst.dataColor.strokeWidth = dataColor.strokeWidth; + } + } +}; + + +Graph3d.prototype._setCameraPosition = function(cameraPosition, dst) { + var camPos = cameraPosition; + if (camPos === undefined) { + return; } - this.frame.style.backgroundColor = fill; - this.frame.style.borderColor = stroke; - this.frame.style.borderWidth = strokeWidth + 'px'; - this.frame.style.borderStyle = 'solid'; + if (dst.camera === undefined) { + dst.camera = new Camera(); + } + + dst.camera.setArmRotation(camPos.horizontal, camPos.vertical); + dst.camera.setArmLength(camPos.distance); }; -/// enumerate the available styles -Graph3d.STYLE = { - BAR: 0, - BARCOLOR: 1, - BARSIZE: 2, - DOT : 3, - DOTLINE : 4, - DOTCOLOR: 5, - DOTSIZE: 6, - GRID : 7, - LINE: 8, - SURFACE : 9 +// +// Public methods for specific settings +// + +/** + * Set the rotation and distance of the camera + * @param {Object} pos An object with the camera position. The object + * contains three parameters: + * - horizontal {Number} + * The horizontal rotation, between 0 and 2*PI. + * Optional, can be left undefined. + * - vertical {Number} + * The vertical rotation, between 0 and 0.5*PI + * if vertical=0.5*PI, the graph is shown from the + * top. Optional, can be left undefined. + * - distance {Number} + * The (normalized) distance of the camera to the + * center of the graph, a value between 0.71 and 5.0. + * Optional, can be left undefined. + */ +Graph3d.prototype.setCameraPosition = function(pos) { + this._setCameraPosition(pos, this); + this.redraw(); }; + +// ----------------------------------------------------------------------------- +// End methods for handling settings +// ----------------------------------------------------------------------------- + + + + /** * Retrieve the style index from given styleName * @param {string} styleName Style name such as 'dot', 'grid', 'dot-line' @@ -334,7 +713,7 @@ Graph3d.prototype._determineColumnIndexes = function(data, style) { } } else { - throw 'Unknown style "' + this.style + '"'; + throw new Error('Unknown style "' + this.style + '"'); } }; @@ -425,13 +804,11 @@ Graph3d.prototype._dataInitialize = function (rawData, style) { this.colX = 'x'; this.colY = 'y'; this.colZ = 'z'; - this.colValue = 'style'; - this.colFilter = 'filter'; - - // check if a filter column is provided if (data[0].hasOwnProperty('filter')) { + this.colFilter = 'filter'; // Bugfix: only set this field if it's actually present! + if (this.dataFilter === undefined) { this.dataFilter = new Filter(rawData, this.colFilter, this); this.dataFilter.setOnLoadCallback(function() {me.redraw();}); @@ -489,17 +866,15 @@ Graph3d.prototype._dataInitialize = function (rawData, style) { if (this.zMax <= this.zMin) this.zMax = this.zMin + 1; this.zStep = (this.defaultZStep !== undefined) ? this.defaultZStep : (this.zMax-this.zMin)/5; - if (this.colValue !== undefined) { + // Bugfix: Only handle field 'style' if it's actually present + if (data[0].hasOwnProperty('style')) { + this.colValue = 'style'; var valueRange = this.getColumnRange(data,this.colValue); this.valueMin = (this.defaultValueMin !== undefined) ? this.defaultValueMin : valueRange.min; this.valueMax = (this.defaultValueMax !== undefined) ? this.defaultValueMax : valueRange.max; if (this.valueMax <= this.valueMin) this.valueMax = this.valueMin + 1; } - // these styles default to having legends - var isLegendGraphStyle = this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE; - this.showLegend = (this.defaultShowLegend !== undefined) ? this.defaultShowLegend : isLegendGraphStyle; - // set the scale dependent on the ranges. this._setScale(); }; @@ -561,6 +936,7 @@ Graph3d.prototype._getDataPoints = function (data) { point3d.x = x; point3d.y = y; point3d.z = z; + point3d.data = data[i]; obj = {}; obj.point = point3d; @@ -588,12 +964,37 @@ Graph3d.prototype._getDataPoints = function (data) { } } else { // 'dot', 'dot-line', etc. + + // Bugfix: ensure value field is present in data if expected + var hasValueField = this.style === Graph3d.STYLE.BARCOLOR + || this.style === Graph3d.STYLE.BARSIZE + || this.style === Graph3d.STYLE.DOTCOLOR + || this.style === Graph3d.STYLE.DOTSIZE; + + if (hasValueField) { + if (this.colValue === undefined) { + throw new Error('Expected data to have ' + + ' field \'style\' ' + + ' for graph style \'' + this.style + '\'' + ); + } + + if (data[0][this.colValue] === undefined) { + throw new Error('Expected data to have ' + + ' field \'' + this.colValue + '\' ' + + ' for graph style \'' + this.style + '\'' + ); + } + } + + // copy all values from the google data table to a list with Point3d objects for (i = 0; i < data.length; i++) { point = new Point3d(); point.x = data[i][this.colX] || 0; point.y = data[i][this.colY] || 0; point.z = data[i][this.colZ] || 0; + point.data = data[i]; if (this.colValue !== undefined) { point.value = data[i][this.colValue] || 0; @@ -701,7 +1102,7 @@ Graph3d.prototype._resizeCanvas = function() { */ Graph3d.prototype.animationStart = function() { if (!this.frame.filter || !this.frame.filter.slider) - throw 'No animation available'; + throw new Error('No animation available'); this.frame.filter.slider.play(); }; @@ -718,64 +1119,33 @@ Graph3d.prototype.animationStop = function() { /** - * Resize the center position based on the current values in this.defaultXCenter - * and this.defaultYCenter (which are strings with a percentage or a value - * in pixels). The center positions are the variables this.xCenter - * and this.yCenter + * Resize the center position based on the current values in this.xCenter + * and this.yCenter (which are strings with a percentage or a value + * in pixels). The center positions are the variables this.currentXCenter + * and this.currentYCenter */ Graph3d.prototype._resizeCenter = function() { // calculate the horizontal center position - if (this.defaultXCenter.charAt(this.defaultXCenter.length-1) === '%') { - this.xcenter = - parseFloat(this.defaultXCenter) / 100 * + if (this.xCenter.charAt(this.xCenter.length-1) === '%') { + this.currentXCenter = + parseFloat(this.xCenter) / 100 * this.frame.canvas.clientWidth; } else { - this.xcenter = parseFloat(this.defaultXCenter); // supposed to be in px + this.currentXCenter = parseFloat(this.xCenter); // supposed to be in px } // calculate the vertical center position - if (this.defaultYCenter.charAt(this.defaultYCenter.length-1) === '%') { - this.ycenter = - parseFloat(this.defaultYCenter) / 100 * + if (this.yCenter.charAt(this.yCenter.length-1) === '%') { + this.currentYCenter = + parseFloat(this.yCenter) / 100 * (this.frame.canvas.clientHeight - this.frame.filter.clientHeight); } else { - this.ycenter = parseFloat(this.defaultYCenter); // supposed to be in px + this.currentYCenter = parseFloat(this.yCenter); // supposed to be in px } }; -/** - * Set the rotation and distance of the camera - * @param {Object} pos An object with the camera position. The object - * contains three parameters: - * - horizontal {Number} - * The horizontal rotation, between 0 and 2*PI. - * Optional, can be left undefined. - * - vertical {Number} - * The vertical rotation, between 0 and 0.5*PI - * if vertical=0.5*PI, the graph is shown from the - * top. Optional, can be left undefined. - * - distance {Number} - * The (normalized) distance of the camera to the - * center of the graph, a value between 0.71 and 5.0. - * Optional, can be left undefined. - */ -Graph3d.prototype.setCameraPosition = function(pos) { - if (pos === undefined) { - return; - } - - if (pos.horizontal !== undefined && pos.vertical !== undefined) { - this.camera.setArmRotation(pos.horizontal, pos.vertical); - } - - if (pos.distance !== undefined) { - this.camera.setArmLength(pos.distance); - } - - this.redraw(); -}; /** @@ -835,87 +1205,13 @@ Graph3d.prototype.setOptions = function (options) { if (options !== undefined) { // retrieve parameter values - if (options.width !== undefined) this.width = options.width; - if (options.height !== undefined) this.height = options.height; - - if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter; - if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter; - - if (options.filterLabel !== undefined) this.filterLabel = options.filterLabel; - if (options.legendLabel !== undefined) this.legendLabel = options.legendLabel; - if (options.showLegend !== undefined) this.defaultShowLegend = options.showLegend; - if (options.xLabel !== undefined) this.xLabel = options.xLabel; - if (options.yLabel !== undefined) this.yLabel = options.yLabel; - if (options.zLabel !== undefined) this.zLabel = options.zLabel; - - if (options.xValueLabel !== undefined) this.xValueLabel = options.xValueLabel; - if (options.yValueLabel !== undefined) this.yValueLabel = options.yValueLabel; - if (options.zValueLabel !== undefined) this.zValueLabel = options.zValueLabel; - if (options.dotSizeRatio !== undefined) this.dotSizeRatio = options.dotSizeRatio; + // Handle the parameters which can be simply copied over + safeCopy(options, this, OPTIONKEYS); + safeCopy(options, this, PREFIXEDOPTIONKEYS, 'default'); - if (options.style !== undefined) { - var styleNumber = this._getStyleNumber(options.style); - if (styleNumber !== -1) { - this.style = styleNumber; - } - } - if (options.showGrid !== undefined) this.showGrid = options.showGrid; - if (options.showPerspective !== undefined) this.showPerspective = options.showPerspective; - if (options.showShadow !== undefined) this.showShadow = options.showShadow; - if (options.tooltip !== undefined) this.showTooltip = options.tooltip; - if (options.showAnimationControls !== undefined) this.showAnimationControls = options.showAnimationControls; - if (options.keepAspectRatio !== undefined) this.keepAspectRatio = options.keepAspectRatio; - if (options.verticalRatio !== undefined) this.verticalRatio = options.verticalRatio; - - if (options.animationInterval !== undefined) this.animationInterval = options.animationInterval; - if (options.animationPreload !== undefined) this.animationPreload = options.animationPreload; - if (options.animationAutoStart !== undefined)this.animationAutoStart = options.animationAutoStart; - - if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth; - if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth; - - if (options.xMin !== undefined) this.defaultXMin = options.xMin; - if (options.xStep !== undefined) this.defaultXStep = options.xStep; - if (options.xMax !== undefined) this.defaultXMax = options.xMax; - if (options.yMin !== undefined) this.defaultYMin = options.yMin; - if (options.yStep !== undefined) this.defaultYStep = options.yStep; - if (options.yMax !== undefined) this.defaultYMax = options.yMax; - if (options.zMin !== undefined) this.defaultZMin = options.zMin; - if (options.zStep !== undefined) this.defaultZStep = options.zStep; - if (options.zMax !== undefined) this.defaultZMax = options.zMax; - if (options.valueMin !== undefined) this.defaultValueMin = options.valueMin; - if (options.valueMax !== undefined) this.defaultValueMax = options.valueMax; - if (options.backgroundColor !== undefined) this._setBackgroundColor(options.backgroundColor); - - if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition; - - if (cameraPosition !== undefined) { - this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical); - this.camera.setArmLength(cameraPosition.distance); - } - - // colors - if (options.axisColor !== undefined) this.axisColor = options.axisColor; - if (options.gridColor !== undefined) this.gridColor = options.gridColor; - if (options.dataColor) { - if (typeof options.dataColor === 'string') { - this.dataColor.fill = options.dataColor; - this.dataColor.stroke = options.dataColor; - } - else { - if (options.dataColor.fill) { - this.dataColor.fill = options.dataColor.fill; - } - if (options.dataColor.stroke) { - this.dataColor.stroke = options.dataColor.stroke; - } - if (options.dataColor.strokeWidth !== undefined) { - this.dataColor.strokeWidth = options.dataColor.strokeWidth; - } - } - } - + // Handle the more complex ('special') fields + this._setSpecialSettings(options, this); } this.setSize(this.width, this.height); @@ -936,7 +1232,7 @@ Graph3d.prototype.setOptions = function (options) { */ Graph3d.prototype.redraw = function() { if (this.dataPoints === undefined) { - throw 'Error: graph data not initialized'; + throw new Error('Graph data not initialized'); } this._resizeCanvas(); @@ -966,6 +1262,17 @@ Graph3d.prototype.redraw = function() { this._redrawLegend(); }; + +/** + * Get drawing context without exposing canvas + */ +Graph3d.prototype._getContext = function() { + var canvas = this.frame.canvas; + var ctx = canvas.getContext('2d'); + return ctx; +}; + + /** * Clear the canvas before redrawing */ @@ -1023,8 +1330,7 @@ Graph3d.prototype._redrawLegend = function() { var left = right - width; var bottom = top + height; - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); + var ctx = this._getContext(); ctx.lineWidth = 1; ctx.font = '14px arial'; // TODO: put in options @@ -1076,10 +1382,8 @@ Graph3d.prototype._redrawLegend = function() { var legendMin = isValueLegend ? this.valueMin : this.zMin; var legendMax = isValueLegend ? this.valueMax : this.zMax; var step = new StepNumber(legendMin, legendMax, (legendMax-legendMin)/5, true); - step.start(); - if (step.getCurrent() < legendMin) { - step.next(); - } + step.start(true); + var y; while (!step.end()) { y = bottom - (step.getCurrent() - legendMin) / (legendMax - legendMin) * height; @@ -1156,8 +1460,7 @@ Graph3d.prototype._redrawSlider = function() { */ Graph3d.prototype._redrawInfo = function() { if (this.dataFilter) { - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); + var ctx = this._getContext(); ctx.font = '14px arial'; // TODO: put in options ctx.lineStyle = 'gray'; @@ -1172,12 +1475,109 @@ Graph3d.prototype._redrawInfo = function() { }; +/** + * Draw a line between 2d points 'from' and 'to'. + * + * If stroke style specified, set that as well. + */ +Graph3d.prototype._line = function(ctx, from, to, strokeStyle) { + if (strokeStyle !== undefined) { + ctx.strokeStyle = strokeStyle; + } + + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x , to.y ); + ctx.stroke(); +} + + +Graph3d.prototype.drawAxisLabelX = function(ctx, point3d, text, armAngle, yMargin) { + if (yMargin === undefined) { + yMargin = 0; + } + + var point2d = this._convert3Dto2D(point3d); + + if (Math.cos(armAngle * 2) > 0) { + ctx.textAlign = 'center'; + ctx.textBaseline = 'top'; + point2d.y += yMargin; + } + else if (Math.sin(armAngle * 2) < 0){ + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + } + else { + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; + } + + ctx.fillStyle = this.axisColor; + ctx.fillText(text, point2d.x, point2d.y); +} + + +Graph3d.prototype.drawAxisLabelY = function(ctx, point3d, text, armAngle, yMargin) { + if (yMargin === undefined) { + yMargin = 0; + } + + var point2d = this._convert3Dto2D(point3d); + + if (Math.cos(armAngle * 2) < 0) { + ctx.textAlign = 'center'; + ctx.textBaseline = 'top'; + point2d.y += yMargin; + } + else if (Math.sin(armAngle * 2) > 0){ + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + } + else { + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; + } + + ctx.fillStyle = this.axisColor; + ctx.fillText(text, point2d.x, point2d.y); +} + + +Graph3d.prototype.drawAxisLabelZ = function(ctx, point3d, text, offset) { + if (offset === undefined) { + offset = 0; + } + + var point2d = this._convert3Dto2D(point3d); + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = this.axisColor; + ctx.fillText(text, point2d.x - offset, point2d.y); +}; + + +/** + + +/** + * Draw a line between 2d points 'from' and 'to'. + * + * If stroke style specified, set that as well. + */ +Graph3d.prototype._line3d = function(ctx, from, to, strokeStyle) { + var from2d = this._convert3Dto2D(from); + var to2d = this._convert3Dto2D(to); + + this._line(ctx, from2d, to2d, strokeStyle); +} + + /** * Redraw the axis */ Graph3d.prototype._redrawAxis = function() { - var canvas = this.frame.canvas, - ctx = canvas.getContext('2d'), + var ctx = this._getContext(), from, to, step, prettyStep, text, xText, yText, zText, offset, xOffset, yOffset, @@ -1188,66 +1588,40 @@ Graph3d.prototype._redrawAxis = function() { ctx.font = 24 / this.camera.getArmLength() + 'px arial'; // calculate the length for the short grid lines - var gridLenX = 0.025 / this.scale.x; - var gridLenY = 0.025 / this.scale.y; + var gridLenX = 0.025 / this.scale.x; + var gridLenY = 0.025 / this.scale.y; var textMargin = 5 / this.camera.getArmLength(); // px - var armAngle = this.camera.getArmRotation().horizontal; + var armAngle = this.camera.getArmRotation().horizontal; + var armVector = new Point2d(Math.cos(armAngle), Math.sin(armAngle)); // draw x-grid lines ctx.lineWidth = 1; prettyStep = (this.defaultXStep === undefined); step = new StepNumber(this.xMin, this.xMax, this.xStep, prettyStep); - step.start(); - if (step.getCurrent() < this.xMin) { - step.next(); - } + step.start(true); + while (!step.end()) { var x = step.getCurrent(); if (this.showGrid) { - from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin)); - to = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin)); - ctx.strokeStyle = this.gridColor; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); + from = new Point3d(x, this.yMin, this.zMin); + to = new Point3d(x, this.yMax, this.zMin); + this._line3d(ctx, from, to, this.gridColor); } else { - from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin)); - to = this._convert3Dto2D(new Point3d(x, this.yMin+gridLenX, this.zMin)); - ctx.strokeStyle = this.axisColor; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); + from = new Point3d(x, this.yMin, this.zMin); + to = new Point3d(x, this.yMin+gridLenX, this.zMin); + this._line3d(ctx, from, to, this.axisColor); - from = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin)); - to = this._convert3Dto2D(new Point3d(x, this.yMax-gridLenX, this.zMin)); - ctx.strokeStyle = this.axisColor; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); + from = new Point3d(x, this.yMax, this.zMin); + to = new Point3d(x, this.yMax-gridLenX, this.zMin); + this._line3d(ctx, from, to, this.axisColor); } - yText = (Math.cos(armAngle) > 0) ? this.yMin : this.yMax; - text = this._convert3Dto2D(new Point3d(x, yText, this.zMin)); - if (Math.cos(armAngle * 2) > 0) { - ctx.textAlign = 'center'; - ctx.textBaseline = 'top'; - text.y += textMargin; - } - else if (Math.sin(armAngle * 2) < 0){ - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - } - else { - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; - } - ctx.fillStyle = this.axisColor; - ctx.fillText(' ' + this.xValueLabel(step.getCurrent()) + ' ', text.x, text.y); + yText = (armVector.x > 0) ? this.yMin : this.yMax; + var point3d = new Point3d(x, yText, this.zMin); + var msg = ' ' + this.xValueLabel(x) + ' '; + this.drawAxisLabelX(ctx, point3d, msg, armAngle, textMargin); step.next(); } @@ -1256,55 +1630,30 @@ Graph3d.prototype._redrawAxis = function() { ctx.lineWidth = 1; prettyStep = (this.defaultYStep === undefined); step = new StepNumber(this.yMin, this.yMax, this.yStep, prettyStep); - step.start(); - if (step.getCurrent() < this.yMin) { - step.next(); - } + step.start(true); + while (!step.end()) { + var y = step.getCurrent(); + if (this.showGrid) { - from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin)); - to = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin)); - ctx.strokeStyle = this.gridColor; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); + from = new Point3d(this.xMin, y, this.zMin); + to = new Point3d(this.xMax, y, this.zMin); + this._line3d(ctx, from, to, this.gridColor); } else { - from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin)); - to = this._convert3Dto2D(new Point3d(this.xMin+gridLenY, step.getCurrent(), this.zMin)); - ctx.strokeStyle = this.axisColor; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); + from = new Point3d(this.xMin, y, this.zMin); + to = new Point3d(this.xMin+gridLenY, y, this.zMin); + this._line3d(ctx, from, to, this.axisColor); - from = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin)); - to = this._convert3Dto2D(new Point3d(this.xMax-gridLenY, step.getCurrent(), this.zMin)); - ctx.strokeStyle = this.axisColor; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); + from = new Point3d(this.xMax, y, this.zMin); + to = new Point3d(this.xMax-gridLenY, y, this.zMin); + this._line3d(ctx, from, to, this.axisColor); } - xText = (Math.sin(armAngle ) > 0) ? this.xMin : this.xMax; - text = this._convert3Dto2D(new Point3d(xText, step.getCurrent(), this.zMin)); - if (Math.cos(armAngle * 2) < 0) { - ctx.textAlign = 'center'; - ctx.textBaseline = 'top'; - text.y += textMargin; - } - else if (Math.sin(armAngle * 2) > 0){ - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - } - else { - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; - } - ctx.fillStyle = this.axisColor; - ctx.fillText(' ' + this.yValueLabel(step.getCurrent()) + ' ', text.x, text.y); + xText = (armVector.y > 0) ? this.xMin : this.xMax; + point3d = new Point3d(xText, y, this.zMin); + var msg = ' ' + this.yValueLabel(y) + ' '; + this.drawAxisLabelY(ctx, point3d, msg, armAngle, textMargin); step.next(); } @@ -1313,133 +1662,84 @@ Graph3d.prototype._redrawAxis = function() { ctx.lineWidth = 1; prettyStep = (this.defaultZStep === undefined); step = new StepNumber(this.zMin, this.zMax, this.zStep, prettyStep); - step.start(); - if (step.getCurrent() < this.zMin) { - step.next(); - } - xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax; - yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax; + step.start(true); + + xText = (armVector.x > 0) ? this.xMin : this.xMax; + yText = (armVector.y < 0) ? this.yMin : this.yMax; + while (!step.end()) { + var z = step.getCurrent(); + // TODO: make z-grid lines really 3d? - from = this._convert3Dto2D(new Point3d(xText, yText, step.getCurrent())); - ctx.strokeStyle = this.axisColor; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(from.x - textMargin, from.y); - ctx.stroke(); + var from3d = new Point3d(xText, yText, z); + var from2d = this._convert3Dto2D(from3d); + to = new Point2d(from2d.x - textMargin, from2d.y); + this._line(ctx, from2d, to, this.axisColor); - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = this.axisColor; - ctx.fillText(this.zValueLabel(step.getCurrent()) + ' ', from.x - 5, from.y); + var msg = this.zValueLabel(z) + ' '; + this.drawAxisLabelZ(ctx, from3d, msg, 5); step.next(); } + ctx.lineWidth = 1; - from = this._convert3Dto2D(new Point3d(xText, yText, this.zMin)); - to = this._convert3Dto2D(new Point3d(xText, yText, this.zMax)); - ctx.strokeStyle = this.axisColor; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); + from = new Point3d(xText, yText, this.zMin); + to = new Point3d(xText, yText, this.zMax); + this._line3d(ctx, from, to, this.axisColor); // draw x-axis ctx.lineWidth = 1; // line at yMin - xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin)); - xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin)); - ctx.strokeStyle = this.axisColor; - ctx.beginPath(); - ctx.moveTo(xMin2d.x, xMin2d.y); - ctx.lineTo(xMax2d.x, xMax2d.y); - ctx.stroke(); + xMin2d = new Point3d(this.xMin, this.yMin, this.zMin); + xMax2d = new Point3d(this.xMax, this.yMin, this.zMin); + this._line3d(ctx, xMin2d, xMax2d, this.axisColor); // line at ymax - xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin)); - xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin)); - ctx.strokeStyle = this.axisColor; - ctx.beginPath(); - ctx.moveTo(xMin2d.x, xMin2d.y); - ctx.lineTo(xMax2d.x, xMax2d.y); - ctx.stroke(); + xMin2d = new Point3d(this.xMin, this.yMax, this.zMin); + xMax2d = new Point3d(this.xMax, this.yMax, this.zMin); + this._line3d(ctx, xMin2d, xMax2d, this.axisColor); // draw y-axis ctx.lineWidth = 1; // line at xMin - from = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin)); - to = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin)); - ctx.strokeStyle = this.axisColor; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); + from = new Point3d(this.xMin, this.yMin, this.zMin); + to = new Point3d(this.xMin, this.yMax, this.zMin); + this._line3d(ctx, from, to, this.axisColor); // line at xMax - from = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin)); - to = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin)); - ctx.strokeStyle = this.axisColor; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); + from = new Point3d(this.xMax, this.yMin, this.zMin); + to = new Point3d(this.xMax, this.yMax, this.zMin); + this._line3d(ctx, from, to, this.axisColor); // draw x-label var xLabel = this.xLabel; if (xLabel.length > 0) { yOffset = 0.1 / this.scale.y; - xText = (this.xMin + this.xMax) / 2; - yText = (Math.cos(armAngle) > 0) ? this.yMin - yOffset: this.yMax + yOffset; - text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin)); - if (Math.cos(armAngle * 2) > 0) { - ctx.textAlign = 'center'; - ctx.textBaseline = 'top'; - } - else if (Math.sin(armAngle * 2) < 0){ - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - } - else { - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; - } - ctx.fillStyle = this.axisColor; - ctx.fillText(xLabel, text.x, text.y); + xText = (this.xMin + this.xMax) / 2; + yText = (armVector.x > 0) ? this.yMin - yOffset: this.yMax + yOffset; + text = new Point3d(xText, yText, this.zMin); + this.drawAxisLabelX(ctx, text, xLabel, armAngle); } // draw y-label var yLabel = this.yLabel; if (yLabel.length > 0) { xOffset = 0.1 / this.scale.x; - xText = (Math.sin(armAngle ) > 0) ? this.xMin - xOffset : this.xMax + xOffset; - yText = (this.yMin + this.yMax) / 2; - text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin)); - if (Math.cos(armAngle * 2) < 0) { - ctx.textAlign = 'center'; - ctx.textBaseline = 'top'; - } - else if (Math.sin(armAngle * 2) > 0){ - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - } - else { - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; - } - ctx.fillStyle = this.axisColor; - ctx.fillText(yLabel, text.x, text.y); + xText = (armVector.y > 0) ? this.xMin - xOffset : this.xMax + xOffset; + yText = (this.yMin + this.yMax) / 2; + text = new Point3d(xText, yText, this.zMin); + + this.drawAxisLabelY(ctx, text, yLabel, armAngle); } // draw z-label var zLabel = this.zLabel; if (zLabel.length > 0) { offset = 30; // pixels. // TODO: relate to the max width of the values on the z axis? - xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax; - yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax; - zText = (this.zMin + this.zMax) / 2; - text = this._convert3Dto2D(new Point3d(xText, yText, zText)); - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = this.axisColor; - ctx.fillText(zLabel, text.x - offset, text.y); + xText = (armVector.x > 0) ? this.xMin : this.xMax; + yText = (armVector.y < 0) ? this.yMin : this.yMax; + zText = (this.zMin + this.zMax) / 2; + text = new Point3d(xText, yText, zText); + + this.drawAxisLabelZ(ctx, text, zLabel, offset); } }; @@ -1476,8 +1776,7 @@ Graph3d.prototype._hsv2rgb = function(H, S, V) { * This function can be used when the style is 'grid' */ Graph3d.prototype._redrawDataGrid = function() { - var canvas = this.frame.canvas, - ctx = canvas.getContext('2d'), + var ctx = this._getContext(), point, right, top, cross, i, topSideVisible, fillStyle, strokeStyle, lineWidth, @@ -1489,24 +1788,7 @@ Graph3d.prototype._redrawDataGrid = function() { if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? - // calculate the translations and screen position of all points - for (i = 0; i < this.dataPoints.length; i++) { - var trans = this._convertPointToTranslation(this.dataPoints[i].point); - var screen = this._convertTranslationToScreen(trans); - - this.dataPoints[i].trans = trans; - this.dataPoints[i].screen = screen; - - // calculate the translation of the point at the bottom (needed for sorting) - var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom); - this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z; - } - - // sort the points on depth of their (x,y) position (not on z) - var sortDepth = function (a, b) { - return b.dist - a.dist; - }; - this.dataPoints.sort(sortDepth); + this._calcTranslations(this.dataPoints); if (this.style === Graph3d.STYLE.SURFACE) { for (i = 0; i < this.dataPoints.length; i++) { @@ -1583,10 +1865,7 @@ Graph3d.prototype._redrawDataGrid = function() { ctx.lineWidth = this._getStrokeWidth(point) * 2; ctx.strokeStyle = this._hsv2rgb(h, 1, 1); - ctx.beginPath(); - ctx.moveTo(point.screen.x, point.screen.y); - ctx.lineTo(right.screen.x, right.screen.y); - ctx.stroke(); + this._line(ctx, point.screen, right.screen); } if (point !== undefined && top !== undefined) { @@ -1596,10 +1875,7 @@ Graph3d.prototype._redrawDataGrid = function() { ctx.lineWidth = this._getStrokeWidth(point) * 2; ctx.strokeStyle = this._hsv2rgb(h, 1, 1); - ctx.beginPath(); - ctx.moveTo(point.screen.x, point.screen.y); - ctx.lineTo(top.screen.x, top.screen.y); - ctx.stroke(); + this._line(ctx, point.screen, top.screen); } } } @@ -1623,30 +1899,13 @@ Graph3d.prototype._getStrokeWidth = function(point) { * This function can be used when the style is 'dot' or 'dot-line' */ Graph3d.prototype._redrawDataDot = function() { - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); + var ctx = this._getContext(); var i; if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? - // calculate the translations of all points - for (i = 0; i < this.dataPoints.length; i++) { - var trans = this._convertPointToTranslation(this.dataPoints[i].point); - var screen = this._convertTranslationToScreen(trans); - this.dataPoints[i].trans = trans; - this.dataPoints[i].screen = screen; - - // calculate the distance from the point at the bottom to the camera - var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom); - this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z; - } - - // order the translated points by depth - var sortDepth = function (a, b) { - return b.dist - a.dist; - }; - this.dataPoints.sort(sortDepth); + this._calcTranslations(this.dataPoints); // draw the datapoints as colored circles var dotSize = this.frame.clientWidth * this.dotSizeRatio; // px @@ -1658,11 +1917,7 @@ Graph3d.prototype._redrawDataDot = function() { //var from = this._convert3Dto2D(new Point3d(point.point.x, point.point.y, this.zMin)); var from = this._convert3Dto2D(point.bottom); ctx.lineWidth = 1; - ctx.strokeStyle = this.gridColor; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(point.screen.x, point.screen.y); - ctx.stroke(); + this._line(ctx, from, point.screen, this.gridColor); } // calculate radius for the circle @@ -1719,30 +1974,13 @@ Graph3d.prototype._redrawDataDot = function() { * This function can be used when the style is 'bar', 'bar-color', or 'bar-size' */ Graph3d.prototype._redrawDataBar = function() { - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); + var ctx = this._getContext(); var i, j, surface, corners; if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? - // calculate the translations of all points - for (i = 0; i < this.dataPoints.length; i++) { - var trans = this._convertPointToTranslation(this.dataPoints[i].point); - var screen = this._convertTranslationToScreen(trans); - this.dataPoints[i].trans = trans; - this.dataPoints[i].screen = screen; - - // calculate the distance from the point at the bottom to the camera - var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom); - this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z; - } - - // order the translated points by depth - var sortDepth = function (a, b) { - return b.dist - a.dist; - }; - this.dataPoints.sort(sortDepth); + this._calcTranslations(this.dataPoints); ctx.lineJoin = 'round'; ctx.lineCap = 'round'; @@ -1861,21 +2099,13 @@ Graph3d.prototype._redrawDataBar = function() { * This function can be used when the style is 'line' */ Graph3d.prototype._redrawDataLine = function() { - var canvas = this.frame.canvas, - ctx = canvas.getContext('2d'), + var ctx = this._getContext(), point, i; if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? - // calculate the translations of all points - for (i = 0; i < this.dataPoints.length; i++) { - var trans = this._convertPointToTranslation(this.dataPoints[i].point); - var screen = this._convertTranslationToScreen(trans); - - this.dataPoints[i].trans = trans; - this.dataPoints[i].screen = screen; - } + this._calcTranslations(this.dataPoints, false); // start the line if (this.dataPoints.length > 0) { diff --git a/lib/graph3d/Slider.js b/lib/graph3d/Slider.js index dd688e50..f6f21ecc 100644 --- a/lib/graph3d/Slider.js +++ b/lib/graph3d/Slider.js @@ -11,7 +11,7 @@ var util = require('../util'); */ function Slider(container, options) { if (container === undefined) { - throw 'Error: No container element defined'; + throw new Error('No container element defined'); } this.container = container; this.visible = (options && options.visible != undefined) ? options.visible : true; @@ -253,7 +253,7 @@ Slider.prototype.setIndex = function(index) { this.onChange(); } else { - throw 'Error: index out of range'; + throw new Error('Index out of range'); } }; diff --git a/lib/graph3d/StepNumber.js b/lib/graph3d/StepNumber.js index 6c11a37c..9675b4f8 100644 --- a/lib/graph3d/StepNumber.js +++ b/lib/graph3d/StepNumber.js @@ -35,6 +35,17 @@ function StepNumber(start, end, step, prettyStep) { this.setRange(start, end, step, prettyStep); }; + +/** + * Check for input values, to prevent disasters from happening + * + * Source: http://stackoverflow.com/a/1830844 + */ +StepNumber.prototype.isNumeric = function(n) { + return !isNaN(parseFloat(n)) && isFinite(n); +}; + + /** * Set a new range: start, end and step. * @@ -45,6 +56,16 @@ function StepNumber(start, end, step, prettyStep) { * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...) */ StepNumber.prototype.setRange = function(start, end, step, prettyStep) { + if (!this.isNumeric(start)) { + throw new Error('Parameter \'start\' is not numeric; value: ' + start); + } + if (!this.isNumeric(end)) { + throw new Error('Parameter \'end\' is not numeric; value: ' + start); + } + if (!this.isNumeric(step)) { + throw new Error('Parameter \'step\' is not numeric; value: ' + start); + } + this._start = start ? start : 0; this._end = end ? end : 0; @@ -115,13 +136,29 @@ StepNumber.prototype.getStep = function () { }; /** - * Set the current value to the largest value smaller than start, which - * is a multiple of the step size + * Set the current to its starting value. + * + * By default, this will be the largest value smaller than start, which + * is a multiple of the step size. + * + * Parameters checkFirst is optional, default false. + * If set to true, move the current value one step if smaller than start. */ -StepNumber.prototype.start = function() { +StepNumber.prototype.start = function(checkFirst) { + if (checkFirst === undefined) { + checkFirst = false; + } + this._current = this._start - this._start % this._step; + + if (checkFirst) { + if (this.getCurrent() < this._start) { + this.next(); + } + } }; + /** * Do a step, add the step size to the current value */ diff --git a/lib/network/locales.js b/lib/network/locales.js index ad5a523a..e71a971f 100644 --- a/lib/network/locales.js +++ b/lib/network/locales.js @@ -53,6 +53,24 @@ exports['es'] = { }; exports['es_ES'] = exports['es']; +//Italiano +exports['it'] = { + edit: 'Modifica', + del: 'Cancella la selezione', + back: 'Indietro', + addNode: 'Aggiungi un nodo', + addEdge: 'Aggiungi un vertice', + editNode: 'Modifica il nodo', + editEdge: 'Modifica il vertice', + addDescription: 'Clicca per aggiungere un nuovo nodo', + edgeDescription: 'Clicca su un nodo e trascinalo ad un altro nodo per connetterli.', + editEdgeDescription: 'Clicca sui Punti di controllo e trascinali ad un nodo per connetterli.', + createEdgeError: 'Non si possono collegare vertici ad un cluster', + deleteClusterError: 'I cluster non possono essere cancellati', + editClusterError: 'I clusters non possono essere modificati.' +}; +exports['it_IT'] = exports['it']; + // Dutch exports['nl'] = { edit: 'Wijzigen', diff --git a/lib/network/modules/Clustering.js b/lib/network/modules/Clustering.js index cb4d503e..f67d0da1 100644 --- a/lib/network/modules/Clustering.js +++ b/lib/network/modules/Clustering.js @@ -348,8 +348,11 @@ class ClusterEngine { * @private */ _cluster(childNodesObj, childEdgesObj, options, refreshData = true) { - // kill condition: no children so can't cluster or only one node in the cluster, don't bother - if (Object.keys(childNodesObj).length < 2) {return;} + // kill condition: no nodes don't bother + if (Object.keys(childNodesObj).length == 0) {return;} + + // allow clusters of 1 if options allow + if (Object.keys(childNodesObj).length == 1 && options.clusterNodeProperties.allowSingleNodeCluster != true) {return;} // check if this cluster call is not trying to cluster anything that is in another cluster. for (let nodeId in childNodesObj) { diff --git a/lib/network/modules/EdgesHandler.js b/lib/network/modules/EdgesHandler.js index 4be26d95..e57e86e5 100644 --- a/lib/network/modules/EdgesHandler.js +++ b/lib/network/modules/EdgesHandler.js @@ -23,9 +23,9 @@ class EdgesHandler { this.options = {}; this.defaultOptions = { arrows: { - to: {enabled: false, scaleFactor:1}, // boolean / {arrowScaleFactor:1} / {enabled: false, arrowScaleFactor:1} - middle: {enabled: false, scaleFactor:1}, - from: {enabled: false, scaleFactor:1} + to: {enabled: false, scaleFactor:1, type: 'arrow'}, // boolean / {arrowScaleFactor:1} / {enabled: false, arrowScaleFactor:1} + middle: {enabled: false, scaleFactor:1, type: 'arrow'}, + from: {enabled: false, scaleFactor:1, type: 'arrow'} }, arrowStrikethrough: true, color: { @@ -388,4 +388,4 @@ class EdgesHandler { } -export default EdgesHandler; \ No newline at end of file +export default EdgesHandler; diff --git a/lib/network/modules/PhysicsEngine.js b/lib/network/modules/PhysicsEngine.js index 7d538906..3555c2ff 100644 --- a/lib/network/modules/PhysicsEngine.js +++ b/lib/network/modules/PhysicsEngine.js @@ -628,7 +628,7 @@ class PhysicsEngine { this._freezeNodes(); } this.stabilizationIterations = 0; - + setTimeout(() => this._stabilizationBatch(),0); } @@ -649,7 +649,7 @@ class PhysicsEngine { this.physicsTick(); count++; } - + if (this.stabilized === false && this.stabilizationIterations < this.targetIterations) { this.body.emitter.emit('stabilizationProgress', {iterations: this.stabilizationIterations, total: this.targetIterations}); setTimeout(this._stabilizationBatch.bind(this),0); @@ -710,11 +710,11 @@ class PhysicsEngine { let angle = Math.atan2(force.y, force.x); ctx.fillStyle = color; - ctx.arrow(node.x + factor*force.x + Math.cos(angle)*arrowSize, node.y + factor*force.y+Math.sin(angle)*arrowSize, angle, arrowSize); + ctx.arrowEndpoint(node.x + factor*force.x + Math.cos(angle)*arrowSize, node.y + factor*force.y+Math.sin(angle)*arrowSize, angle, arrowSize); ctx.fill(); } } - + } -export default PhysicsEngine; \ No newline at end of file +export default PhysicsEngine; diff --git a/lib/network/modules/SelectionHandler.js b/lib/network/modules/SelectionHandler.js index 8b48c1f6..8c5eaf37 100644 --- a/lib/network/modules/SelectionHandler.js +++ b/lib/network/modules/SelectionHandler.js @@ -534,8 +534,9 @@ class SelectionHandler { this.hoverObj.edges[edgeId].hover = false; delete this.hoverObj.edges[edgeId]; } - // if the blur remains the same and the object is undefined (mouse off), we blur the edge - else if (object === undefined) { + // if the blur remains the same and the object is undefined (mouse off) or another + // edge has been hovered, we blur the edge + else if (object === undefined || object instanceof Edge) { this.blurObject(this.hoverObj.edges[edgeId]); delete this.hoverObj.edges[edgeId]; hoverChanged = true; diff --git a/lib/network/modules/components/edges/BezierEdgeDynamic.js b/lib/network/modules/components/edges/BezierEdgeDynamic.js index 0108d50a..c35d641a 100644 --- a/lib/network/modules/components/edges/BezierEdgeDynamic.js +++ b/lib/network/modules/components/edges/BezierEdgeDynamic.js @@ -133,8 +133,16 @@ class BezierEdgeDynamic extends BezierEdgeBase { */ getPoint(percentage, viaNode = this.via) { let t = percentage; - let x = Math.pow(1 - t, 2) * this.fromPoint.x + (2 * t * (1 - t)) * viaNode.x + Math.pow(t, 2) * this.toPoint.x; - let y = Math.pow(1 - t, 2) * this.fromPoint.y + (2 * t * (1 - t)) * viaNode.y + Math.pow(t, 2) * this.toPoint.y; + let x, y; + if (this.from === this.to){ + let [cx,cy,cr] = this._getCircleData(this.from) + let a = 2 * Math.PI * (1 - t); + x = cx + cr * Math.sin(a); + y = cy + cr - cr * (1 - Math.cos(a)); + } else { + x = Math.pow(1 - t, 2) * this.fromPoint.x + 2 * t * (1 - t) * viaNode.x + Math.pow(t, 2) * this.toPoint.x; + y = Math.pow(1 - t, 2) * this.fromPoint.y + 2 * t * (1 - t) * viaNode.y + Math.pow(t, 2) * this.toPoint.y; + } return {x: x, y: y}; } @@ -151,4 +159,4 @@ class BezierEdgeDynamic extends BezierEdgeBase { } -export default BezierEdgeDynamic; \ No newline at end of file +export default BezierEdgeDynamic; diff --git a/lib/network/modules/components/edges/util/EdgeBase.js b/lib/network/modules/components/edges/util/EdgeBase.js index 659313a1..1371ab4e 100644 --- a/lib/network/modules/components/edges/util/EdgeBase.js +++ b/lib/network/modules/components/edges/util/EdgeBase.js @@ -412,6 +412,7 @@ class EdgeBase { let node2; let guideOffset; let scaleFactor; + let type; let lineWidth = this.getLineWidth(selected, hover); if (position === 'from') { @@ -419,17 +420,20 @@ class EdgeBase { node2 = this.to; guideOffset = 0.1; scaleFactor = this.options.arrows.from.scaleFactor; + type = this.options.arrows.from.type; } else if (position === 'to') { node1 = this.to; node2 = this.from; guideOffset = -0.1; scaleFactor = this.options.arrows.to.scaleFactor; + type = this.options.arrows.to.type; } else { node1 = this.to; node2 = this.from; scaleFactor = this.options.arrows.middle.scaleFactor; + type = this.options.arrows.middle.type; } // if not connected to itself @@ -475,7 +479,7 @@ class EdgeBase { var yi = arrowPoint.y - length * 0.9 * Math.sin(angle); let arrowCore = {x: xi, y: yi}; - return {point: arrowPoint, core: arrowCore, angle: angle, length: length}; + return {point: arrowPoint, core: arrowCore, angle: angle, length: length, type: type}; } /** @@ -491,8 +495,13 @@ class EdgeBase { ctx.fillStyle = ctx.strokeStyle; ctx.lineWidth = this.getLineWidth(selected, hover); - // draw arrow at the end of the line - ctx.arrow(arrowData.point.x, arrowData.point.y, arrowData.angle, arrowData.length); + if (arrowData.type && arrowData.type.toLowerCase() === 'circle') { + // draw circle at the end of the line + ctx.circleEndpoint(arrowData.point.x, arrowData.point.y, arrowData.angle, arrowData.length); + } else { + // draw arrow at the end of the line + ctx.arrowEndpoint(arrowData.point.x, arrowData.point.y, arrowData.angle, arrowData.length); + } // draw shadow if enabled this.enableShadow(ctx); @@ -521,4 +530,4 @@ class EdgeBase { } } -export default EdgeBase; \ No newline at end of file +export default EdgeBase; diff --git a/lib/network/options.js b/lib/network/options.js index 9c0ff2b8..5b9e9f7a 100644 --- a/lib/network/options.js +++ b/lib/network/options.js @@ -24,9 +24,9 @@ let allOptions = { }, edges: { arrows: { - to: { enabled: { boolean }, scaleFactor: { number }, __type__: { object, boolean } }, - middle: { enabled: { boolean }, scaleFactor: { number }, __type__: { object, boolean } }, - from: { enabled: { boolean }, scaleFactor: { number }, __type__: { object, boolean } }, + to: { enabled: { boolean }, scaleFactor: { number }, type: { string: ['arrow', 'circle'] }, __type__: { object, boolean } }, + middle: { enabled: { boolean }, scaleFactor: { number }, type: { string: ['arrow', 'circle'] }, __type__: { object, boolean } }, + from: { enabled: { boolean }, scaleFactor: { number }, type: { string: ['arrow', 'circle'] }, __type__: { object, boolean } }, __type__: { string: ['from', 'to', 'middle'], object } }, arrowStrikethrough: { boolean }, @@ -371,9 +371,9 @@ let configureOptions = { }, edges: { arrows: { - to: { enabled: false, scaleFactor: [1, 0, 3, 0.05] }, // boolean / {arrowScaleFactor:1} / {enabled: false, arrowScaleFactor:1} - middle: { enabled: false, scaleFactor: [1, 0, 3, 0.05] }, - from: { enabled: false, scaleFactor: [1, 0, 3, 0.05] } + to: { enabled: false, scaleFactor: [1, 0, 3, 0.05], type: 'arrow' }, + middle: { enabled: false, scaleFactor: [1, 0, 3, 0.05], type: 'arrow' }, + from: { enabled: false, scaleFactor: [1, 0, 3, 0.05], type: 'arrow' } }, arrowStrikethrough: true, color: { @@ -502,13 +502,7 @@ let configureOptions = { solver: ['barnesHut', 'forceAtlas2Based', 'repulsion', 'hierarchicalRepulsion'], timestep: [0.5, 0.01, 1, 0.01], //adaptiveTimestep: true - }, - global: { - locale: ['en', 'nl'] } }; - - - export {allOptions, configureOptions}; diff --git a/lib/network/shapes.js b/lib/network/shapes.js index f2232bfc..9346f482 100644 --- a/lib/network/shapes.js +++ b/lib/network/shapes.js @@ -206,9 +206,9 @@ if (typeof CanvasRenderingContext2D !== 'undefined') { /** - * Draw an arrow point (no line) + * Draw an arrow at the end of a line with the given angle. */ - CanvasRenderingContext2D.prototype.arrow = function (x, y, angle, length) { + CanvasRenderingContext2D.prototype.arrowEndpoint = function (x, y, angle, length) { // tail var xt = x - length * Math.cos(angle); var yt = y - length * Math.sin(angle); @@ -233,6 +233,16 @@ if (typeof CanvasRenderingContext2D !== 'undefined') { this.closePath(); }; + /** + * Draw an circle an the end of an line with the given angle. + */ + CanvasRenderingContext2D.prototype.circleEndpoint = function (x, y, angle, length) { + var radius = length * 0.4; + var xc = x - radius * Math.cos(angle); + var yc = y - radius * Math.sin(angle); + this.circle(xc, yc, radius); + }; + /** * Sets up the dashedLine functionality for drawing * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas diff --git a/lib/timeline/Core.js b/lib/timeline/Core.js index cd53472a..cca32f9c 100644 --- a/lib/timeline/Core.js +++ b/lib/timeline/Core.js @@ -222,8 +222,7 @@ Core.prototype.setOptions = function (options) { var fields = [ 'width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'clickToUse', 'dataAttributes', 'hiddenDates', - 'locale', 'locales', 'moment', 'rtl', - 'throttleRedraw' + 'locale', 'locales', 'moment', 'rtl' ]; util.selectiveExtend(fields, this.options, options); @@ -233,7 +232,7 @@ Core.prototype.setOptions = function (options) { this.dom.leftContainer = this.dom.rightContainer; this.dom.rightContainer = contentContainer; this.dom.container.style.direction = "rtl"; - this.dom.backgroundVertical.className = 'vis-panel vis-background vis-vertical-rtl'; } + this.dom.backgroundVertical.className = 'vis-panel vis-background vis-vertical-rtl'; } this.options.orientation = {item:undefined,axis:undefined}; if ('orientation' in options) { @@ -330,7 +329,7 @@ Core.prototype.setOptions = function (options) { // override redraw with a throttled version if (!this._origRedraw) { this._origRedraw = this._redraw.bind(this); - this._redraw = util.throttle(this._origRedraw, this.options.throttleRedraw); + this._redraw = util.throttle(this._origRedraw); } else { // Not the initial run: redraw everything this._redraw(); diff --git a/lib/timeline/TimeStep.js b/lib/timeline/TimeStep.js index c5a9bfc2..dc3991ed 100644 --- a/lib/timeline/TimeStep.js +++ b/lib/timeline/TimeStep.js @@ -532,6 +532,10 @@ TimeStep.prototype.getLabelMinor = function(date) { date = this.current; } + if (typeof(this.format.minorLabels) === "function") { + return this.format.minorLabels(date, this.scale, this.step); + } + var format = this.format.minorLabels[this.scale]; return (format && format.length > 0) ? this.moment(date).format(format) : ''; }; @@ -546,6 +550,10 @@ TimeStep.prototype.getLabelMajor = function(date) { if (date == undefined) { date = this.current; } + + if (typeof(this.format.majorLabels) === "function") { + return this.format.majorLabels(date, this.scale, this.step); + } var format = this.format.majorLabels[this.scale]; return (format && format.length > 0) ? this.moment(date).format(format) : ''; diff --git a/lib/timeline/Timeline.js b/lib/timeline/Timeline.js index 4b7cde2a..a8e55e92 100644 --- a/lib/timeline/Timeline.js +++ b/lib/timeline/Timeline.js @@ -47,7 +47,6 @@ function Timeline (container, items, groups, options) { end: null, autoResize: true, - throttleRedraw: 0, // ms orientation: { axis: 'bottom', // axis orientation: 'bottom', 'top', or 'both' @@ -426,7 +425,7 @@ Timeline.prototype.getItemRange = function () { var start = getStart(item); var end = getEnd(item); - + if (this.options.rtl) { var startSide = start - (item.getWidthRight() + 10) * factor; var endSide = end + (item.getWidthLeft() + 10) * factor; @@ -434,7 +433,7 @@ Timeline.prototype.getItemRange = function () { var startSide = start - (item.getWidthLeft() + 10) * factor; var endSide = end + (item.getWidthRight() + 10) * factor; } - + if (startSide < min) { min = startSide; diff --git a/lib/timeline/component/Group.js b/lib/timeline/component/Group.js index 512fac58..f522fd82 100644 --- a/lib/timeline/component/Group.js +++ b/lib/timeline/component/Group.js @@ -88,7 +88,7 @@ Group.prototype.setData = function(data) { // update contents var content; if (this.itemSet.options && this.itemSet.options.groupTemplate) { - content = this.itemSet.options.groupTemplate(data); + content = this.itemSet.options.groupTemplate(data, this.dom.inner); } else { content = data && data.content; @@ -180,8 +180,16 @@ Group.prototype.redraw = function(range, margin, restack) { // recalculate the height of the subgroups this._calculateSubGroupHeights(); + this.isVisible = this._isGroupVisible(range, margin); + // calculate actual size and position + var foreground = this.dom.foreground; + this.top = foreground.offsetTop; + this.right = foreground.offsetLeft; + this.width = foreground.offsetWidth; + + this.isVisible = this._isGroupVisible(range, margin); // reposition visible items vertically if (typeof this.itemSet.options.order === 'function') { // a custom order function @@ -220,7 +228,7 @@ Group.prototype.redraw = function(range, margin, restack) { stack.nostack(this.visibleItems, margin, this.subgroups); } } - + if (!this.isVisible && this.height) { return resized = false; } @@ -275,7 +283,6 @@ Group.prototype._calculateSubGroupHeights = function () { * check if group is visible * @private */ - Group.prototype._isGroupVisible = function (range, margin) { var isVisible = (this.top <= range.body.domProps.centerContainer.height - range.body.domProps.scrollTop + margin.axis) @@ -556,20 +563,7 @@ Group.prototype._updateVisibleItems = function(orderedItems, oldVisibleItems, ra // reposition item horizontally item.repositionX(); } - - // debug - //console.log("new line") - //if (this.groupId == null) { - // for (i = 0; i < orderedItems.byStart.length; i++) { - // item = orderedItems.byStart[i].data; - // console.log('start',i,initialPosByStart, item.start.valueOf(), item.content, item.start >= lowerBound && item.start <= upperBound,i == initialPosByStart ? "<------------------- HEREEEE" : "") - // } - // for (i = 0; i < orderedItems.byEnd.length; i++) { - // item = orderedItems.byEnd[i].data; - // console.log('rangeEnd',i,initialPosByEnd, item.end.valueOf(), item.content, item.end >= range.start && item.end <= range.end,i == initialPosByEnd ? "<------------------- HEREEEE" : "") - // } - //} - + return visibleItems; }; diff --git a/lib/timeline/component/item/BackgroundItem.js b/lib/timeline/component/item/BackgroundItem.js index cdebc4ed..44048527 100644 --- a/lib/timeline/component/item/BackgroundItem.js +++ b/lib/timeline/component/item/BackgroundItem.js @@ -47,7 +47,7 @@ BackgroundItem.prototype.stack = false; */ BackgroundItem.prototype.isVisible = function(range) { // determine visibility - return (this.data.start < range.end) && (this.data.end > range.start); + return (this.data.start < range.end) && (this.data.end > range.start); }; /** diff --git a/lib/timeline/component/item/BoxItem.js b/lib/timeline/component/item/BoxItem.js index 4a6d5e9e..f77652bf 100644 --- a/lib/timeline/component/item/BoxItem.js +++ b/lib/timeline/component/item/BoxItem.js @@ -42,9 +42,22 @@ BoxItem.prototype = new Item (null, null, null); */ BoxItem.prototype.isVisible = function(range) { // determine visibility - // TODO: account for the real width of the item. Right now we just add 1/4 to the window - var interval = (range.end - range.start) / 4; - return (this.data.start > range.start - interval) && (this.data.start < range.end + interval); + var isVisible; + var align = this.options.align; + var msPerPixel = (range.end - range.start) / range.body.dom.center.clientWidth; + var widthInMs = this.width * msPerPixel; + + if (align == 'right') { + isVisible = (this.data.start.getTime() > range.start ) && (this.data.start.getTime() - widthInMs < range.end); + } + else if (align == 'left') { + isVisible = (this.data.start.getTime() + widthInMs > range.start ) && (this.data.start.getTime() < range.end); + } + else { + // default or 'center' + isVisible = (this.data.start.getTime() + widthInMs/2 > range.start ) && (this.data.start.getTime() - widthInMs/2 < range.end); + } + return isVisible; }; /** diff --git a/lib/timeline/component/item/Item.js b/lib/timeline/component/item/Item.js index ea5c7cbb..e50918a8 100644 --- a/lib/timeline/component/item/Item.js +++ b/lib/timeline/component/item/Item.js @@ -99,7 +99,6 @@ Item.prototype.setParent = function(parent) { * @returns {boolean} True if visible */ Item.prototype.isVisible = function(range) { - // Should be implemented by Item implementations return false; }; @@ -190,7 +189,7 @@ Item.prototype._updateContents = function (element) { var content; if (this.options.template) { var itemData = this.parent.itemSet.itemsData.get(this.id); // get a clone of the data from the dataset - content = this.options.template(itemData); + content = this.options.template(itemData, element); } else { content = this.data.content; diff --git a/lib/timeline/component/item/PointItem.js b/lib/timeline/component/item/PointItem.js index a05b9982..10a2463c 100644 --- a/lib/timeline/component/item/PointItem.js +++ b/lib/timeline/component/item/PointItem.js @@ -43,9 +43,10 @@ PointItem.prototype = new Item (null, null, null); */ PointItem.prototype.isVisible = function(range) { // determine visibility - // TODO: account for the real width of the item. Right now we just add 1/4 to the window - var interval = (range.end - range.start) / 4; - return (this.data.start > range.start - interval) && (this.data.start < range.end + interval); + var msPerPixel = (range.end - range.start) / range.body.dom.center.clientWidth; + var widthInMs = this.width * msPerPixel; + + return (this.data.start.getTime() + widthInMs > range.start ) && (this.data.start < range.end); }; /** diff --git a/lib/timeline/component/item/RangeItem.js b/lib/timeline/component/item/RangeItem.js index 98614607..8ec29991 100644 --- a/lib/timeline/component/item/RangeItem.js +++ b/lib/timeline/component/item/RangeItem.js @@ -43,14 +43,8 @@ RangeItem.prototype.baseClassName = 'vis-item vis-range'; */ RangeItem.prototype.isVisible = function(range) { // determine visibility -var isVisible = -// determine horizontal visibillity - (this.data.start < range.end) && - (this.data.end > range.start) && -// determine verticaltal visibillity - (this.parent.top < range.body.domProps.centerContainer.height - range.body.domProps.scrollTop) && - (this.parent.top + this.parent.height > - range.body.domProps.scrollTop) -return isVisible;}; + return (this.data.start < range.end) && (this.data.end > range.start); +}; /** * Repaint the item diff --git a/lib/timeline/locales.js b/lib/timeline/locales.js index fade73eb..50cbe03b 100644 --- a/lib/timeline/locales.js +++ b/lib/timeline/locales.js @@ -6,6 +6,12 @@ exports['en'] = { exports['en_EN'] = exports['en']; exports['en_US'] = exports['en']; +// Italiano +exports['it'] = { + current: 'attuale', + time: 'tempo' +}; + // Dutch exports['nl'] = { current: 'huidige', diff --git a/lib/timeline/optionsGraph2d.js b/lib/timeline/optionsGraph2d.js index fb8e0455..30b27b75 100644 --- a/lib/timeline/optionsGraph2d.js +++ b/lib/timeline/optionsGraph2d.js @@ -100,7 +100,6 @@ let allOptions = { }, autoResize: {boolean}, - throttleRedraw: {number}, clickToUse: {boolean}, end: {number, date, string, moment}, format: { @@ -225,7 +224,6 @@ let configureOptions = { }, autoResize: true, - throttleRedraw: [10, 0, 1000, 10], clickToUse: false, end: '', format: { diff --git a/lib/timeline/optionsTimeline.js b/lib/timeline/optionsTimeline.js index 0069a3db..a20d27f4 100644 --- a/lib/timeline/optionsTimeline.js +++ b/lib/timeline/optionsTimeline.js @@ -27,7 +27,6 @@ let allOptions = { align: {string}, rtl: {boolean, 'undefined': 'undefined'}, autoResize: {boolean}, - throttleRedraw: {number}, clickToUse: {boolean}, dataAttributes: {string, array}, editable: { @@ -48,7 +47,7 @@ let allOptions = { day: {string,'undefined': 'undefined'}, month: {string,'undefined': 'undefined'}, year: {string,'undefined': 'undefined'}, - __type__: {object} + __type__: {object, 'function': 'function'} }, majorLabels: { millisecond: {string,'undefined': 'undefined'}, @@ -59,7 +58,7 @@ let allOptions = { day: {string,'undefined': 'undefined'}, month: {string,'undefined': 'undefined'}, year: {string,'undefined': 'undefined'}, - __type__: {object} + __type__: {object, 'function': 'function'} }, __type__: {object} }, @@ -145,7 +144,6 @@ let configureOptions = { align: ['center', 'left', 'right'], direction: false, autoResize: true, - throttleRedraw: [10, 0, 1000, 10], clickToUse: false, // dataAttributes: ['all'], // FIXME: can be 'all' or string[] editable: { @@ -229,4 +227,4 @@ let configureOptions = { } }; -export {allOptions, configureOptions}; \ No newline at end of file +export {allOptions, configureOptions}; diff --git a/lib/util.js b/lib/util.js index 6e90ffc2..c1dc1c21 100644 --- a/lib/util.js +++ b/lib/util.js @@ -702,29 +702,20 @@ exports.updateProperty = function (object, key, value) { }; /** - * Throttle the given function to be only executed once every `wait` milliseconds + * Throttle the given function to be only executed once per animation frame * @param {function} fn - * @param {number} wait Time in milliseconds * @returns {function} Returns the throttled function */ -exports.throttle = function (fn, wait) { - var timeout = null; - var needExecution = false; +exports.throttle = function (fn) { + var scheduled = false; return function throttled () { - if (!timeout) { - needExecution = false; - fn(); - - timeout = setTimeout(function() { - timeout = null; - if (needExecution) { - throttled(); - } - }, wait) - } - else { - needExecution = true; + if (!scheduled) { + scheduled = true; + requestAnimationFrame(function () { + scheduled = false; + fn(); + }); } } }; diff --git a/misc/how_to_help.md b/misc/how_to_help.md new file mode 100644 index 00000000..fe16e1e6 --- /dev/null +++ b/misc/how_to_help.md @@ -0,0 +1,67 @@ +# HowTo Help + +The company that developed vis.js for the main part, *almende* is [not able to maintain the project at the moment](./we_need_help.md). So help from the community is very needed and welcome! + +## There are many ways to help: + +### Answering questions + +There are new [issues with questions](//github.com/almende/vis/issues?q=is%3Aissue+is%3Aopen+label%3Aquestion+sort%3Acreated-desc) how to use vis.js opened almost every day. Be part of the community and help answer them! + +A better way to ask questions on how to use vis.js is [stackoverflow](https://stackoverflow.com/tags/vis.js). Questions are posed here also and need to be answered by the community. [Please help answering questions](https://stackoverflow.com/tags/vis.js) here also. + +### Closing old issues + +A new issue is often opened fast and then forgotten. Please help go trough [the old issues](//github.com/almende/vis/issues?q=is%3Aissue+is%3Aopen+sort%3Acreated-asc) (especially the [questions](//github.com/almende/vis/issues?q=is%3Aissue+is%3Aopen+sort%3Acreated-asc+label%3Aquestion)) and ask the creator of the issues if the problem still exists before closing the issue. The support team uses the **issue inactive** label to mark these issues. + +### Improve the webpage + +The visjs.org webpage is hosted on the [gh-pages branch](//github.com/almende/vis/tree/gh-pages). If you find a typo or anything else that should be improved feel free to create a pull-request to *gh-pages*. Please make changes in your own fork of gh-pages so the support team can view the changes in your hosted fork. + +### Create new examples + +We have [a collection of examples](//github.com/almende/vis/tree/develop/examples). Please help by creating interesting new ones that show a specific problem or layout. Keep the examples easy to understand for beginners and remove unnecessary clutter. + +### Provide interesting showcases + +If you use vis.js to develop something beautiful feel free to create a pull-request to our show cases page in the gh-pages branch](//github.com/almende/vis/tree/gh-pages/showcase). [These showcases are displayed on our webpage](http://visjs.org/showcase/index.html) and we are always looking for new examples. + +### Confirming and fixing bugs + +Every software has bugs. We also have [quite a nice collection](https://github.com/almende/vis/issues?q=is%3Aissue+is%3Aopen+label%3Abug+sort%3Areactions-%2B1-desc) ;-) +Feel free to fix as many bugs as you want! + +You can not only help by fixing bugs, but also by confirming the bug or even creating a minimal code example to prove this bug exists. + +### Implementing Feature-Requests + +A lot of people have a lot of ideas for improving vis.js. [We label these issues as **enhancement**](https://github.com/almende/vis/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement). Feel free to implement a new feature by creating a new Pull-Request. + +[Some issues are labeled **For everybody!**](//github.com/almende/vis/issues?q=is%3Aissue+is%3Aopen+label%3A%22For+everyone%21%22+sort%3Areactions-%2B1-desc). These are a good starting point. + +### Reviewing Pull-Requests + +We use [GitHub's two-step review](//help.github.com/articles/about-pull-request-reviews/) to make sure pull-requests are clean. You can help by checking out pull-request branches and testing them. You also can comment on lines of code and make sure the pull-request introduces no new bugs or typos. + +## Creating Pull Requests + +There are some rules for pull-request: + +* All pull-request must be to the [develop-branch](//github.com/almende/vis/tree/develop). Pull-request against the [master-branch](//github.com/almende/vis/tree/master) must be closed. (Changes to [gh-pages](//github.com/almende/vis/tree/gh-pages) are also ok.) + +* Only commit changes done in the source files in the folder `lib`, not to the builds + which are located in the folder `dist`. + +* Keep your changes small and clear. Only work on one topic at one time and only change lines of code that you have to change to reach your goal. + +* Test your changes before creating a pull-request. The easiest way is to open the existing examples and playing with them. + +* If you are fixing or implementing an existing issue, please refer to it in the description and in the commit message. + +* If you are introducing a new feature, add some documentation and a new example to make it easy to adapt. + +* If you introduce breaking changes, like changing the signature of a public function, point that out in your description. Breaking changes result in a new major release. + +* Always adapt to the code style of the existing source. Never adapt existing code to your personal taste. :trollface: + +**Happy Helping!!** diff --git a/misc/we_need_help.md b/misc/we_need_help.md new file mode 100644 index 00000000..a3eb9106 --- /dev/null +++ b/misc/we_need_help.md @@ -0,0 +1,15 @@ +# We need help! + +## The current status + +Vis.js is looking for people who can help maintain and improve the library. We've put a lot of effort in building these visualizations, fixing bugs, and supporting users as much as we can. For some time now, we’ve been lacking the manpower to maintain the library the way we have in recent years. [@josdejong](//github.com/josdejong) has left the company for a new opportunity, and [@AlexDM0](//github.com/AlexDM0) has moved internally to a daughter company, with severe impact on his time and availability for Vis.js. At the moment [@ludost](//github.com/ludost) is the official maintainer from Almende, but does not have much time to help out. + +Although Almende is looking to replace the expertise required for Vis.js, we don't expect to be able to do comprehensive project management any time soon. At the same time we’d like to spare Vis.js from becoming abandonware, especially given the relative healthy user base. For the longer term future we would be happy if vis.js could stand on its own feet, community supported. + +**If you want to support the project please just start by [helping out](./how_to_help.md).** + +If you have shown some commitment to the project you can ask [@ludost](//github.com/ludost) to become a member of the community support team. This team has write permissions to the repository and is helping maintaining it. Currently this team consists of: + +* [@ludost](//github.com/ludost) (almende maintainer) +* [@mojoaxel](//github.com/mojoaxel) +* [@yotamberk](//github.com/yotamberk) diff --git a/package.json b/package.json index 3b5d25ba..6d82f185 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,13 @@ "watch": "gulp watch", "watch-dev": "gulp watch --bundle" }, - "dependencies": {}, + "dependencies": { + "emitter-component": "^1.1.1", + "moment": "^2.12.0", + "propagating-hammerjs": "^1.4.6", + "hammerjs": "^2.0.6", + "keycharm": "^0.2.0" + }, "devDependencies": { "async": "^2.0.0-rc.2", "babel-core": "^6.6.5", @@ -36,18 +42,13 @@ "babel-preset-es2015": "^6.6.0", "babelify": "^7.2.0", "clean-css": "^3.4.10", - "emitter-component": "^1.1.1", "gulp": "^3.9.1", "gulp-clean-css": "^2.0.11", "gulp-concat": "^2.6.0", "gulp-rename": "^1.2.2", "gulp-util": "^3.0.7", - "hammerjs": "^2.0.6", - "keycharm": "^0.2.0", "merge-stream": "^1.0.0", - "mocha": "^2.5.3", - "moment": "^2.12.0", - "propagating-hammerjs": "^1.4.6", + "mocha": "^2.4.5", "rimraf": "^2.5.2", "uglify-js": "^2.6.2", "uuid": "^2.0.1",