diff --git a/README.md b/README.md index 9b5f86d4..f4f1c188 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ slow, so when only the non-minified library is needed, one can use the ## Custom builds -The folder `dist` contains bundled versions of vis.js for direct use in the browser. These bundles contain the all visualizations and includes external dependencies such as hammer.js and moment.js. +The folder `dist` contains bundled versions of vis.js for direct use in the browser. These bundles contain all the visualizations and include external dependencies such as hammer.js and moment.js. The source code of vis.js consists of commonjs modules, which makes it possible to create custom bundles using tools like [Browserify](http://browserify.org/) or [Webpack](http://webpack.github.io/). This can be bundling just one visualization like the Timeline, or bundling vis.js as part of your own browserified web application. diff --git a/docs/data/dataset.html b/docs/data/dataset.html index 3f90ead4..af0f2b4d 100644 --- a/docs/data/dataset.html +++ b/docs/data/dataset.html @@ -598,9 +598,11 @@ function (event, properties, senderId) { update, and remove, properties is always an object containing a property items, which contains an array with the ids of the affected - items. The update event has an extra field oldData - containing the original data of the updated items, and a field data - containing the changes: the properties of the items that are being updated. + items. The update and remove events have an extra + field oldData containing the original data of the items in the + dataset before the items were updated or removed. The update + event also contains a field data containing the changes: + the properties of the items that are being updated. diff --git a/docs/graph2d/index.html b/docs/graph2d/index.html index ed062202..8f07e20a 100644 --- a/docs/graph2d/index.html +++ b/docs/graph2d/index.html @@ -1202,7 +1202,7 @@ function (option, path) { on(event, callback) none - Create an event listener. The callback function is invoked every time the event is triggered. Avialable events: rangechange, rangechanged, select. The callback function is invoked as callback(properties), where properties is an object containing event specific properties. See section Events for more information. + Create an event listener. The callback function is invoked every time the event is triggered. Available events: rangechange, rangechanged, select. The callback function is invoked as callback(properties), where properties is an object containing event specific properties. See section Events for more information. diff --git a/docs/timeline/index.html b/docs/timeline/index.html index cd02f3d2..c7051a57 100644 --- a/docs/timeline/index.html +++ b/docs/timeline/index.html @@ -675,6 +675,15 @@ function (option, path) { + + horizontalScroll + Boolean + false + This option allows you to scroll horizontally to move backwards and forwards in the time range. + Only applicable when option zoomCtrl is defined or zoomable is false. Notice that defining this option as true will override verticalScroll scroll event but not eliminate the vertical scrollbar. + + + itemsAlwaysDraggable boolean @@ -1015,6 +1024,14 @@ function (option, path) { + + verticalScroll + Boolean + false + Show a vertical scroll on the side of the group list and link it to the scroll event when zoom is not triggered. Notice that defining this option as true will NOT override horizontalScroll. The scroll event will be vertically ignored, but a vertical scrollbar will be visible + + + width String or Number @@ -1194,7 +1211,7 @@ document.getElementById('myTimeline').onclick = function (event) { on(event, callback) none - Create an event listener. The callback function is invoked every time the event is triggered. Avialable events: rangechange, rangechanged, select, itemover, itemout. The callback function is invoked as callback(properties), where properties is an object containing event specific properties. See section Events for more information. + Create an event listener. The callback function is invoked every time the event is triggered. Available events: rangechange, rangechanged, select, itemover, itemout. The callback function is invoked as callback(properties), where properties is an object containing event specific properties. See section Events for more information. @@ -1233,7 +1250,7 @@ document.getElementById('myTimeline').onclick = function (event) { none Adjust the time of a custom time bar. Parameter time can be a Date object, numeric timestamp, or ISO date string. - Parameter id is the idof the custom time bar, and is undefined by default. + Parameter id is the id of the custom time bar, and is undefined by default. @@ -1557,7 +1574,9 @@ var items = new vis.DataSet([

@@ -1583,7 +1602,7 @@ var items = new vis.DataSet([

Templates

- Timeline supports templates to format item contents. Any template engine (such as handlebars or mustache) can be used, and one can also manually build HTML. In the options, one can provide a template handler. This handler is a function accepting an items data as argument, and outputs formatted HTML: + Timeline supports templates to format item contents. Any template engine (such as handlebars or mustache) can be used, and one can also manually build HTML. In the options, one can provide a template handler. This handler is a function accepting an item's data as argument, and outputs formatted HTML:

var options = {
@@ -1680,7 +1699,7 @@ var options = {
 
   

Create a new locale

- To load a locale into the Timeline not supported by default, one can add a new locale to the option locales: + To load a locale (that is not supported by default) into the Timeline, one can add a new locale to the option locales:
var options = {
   locales: {
@@ -1736,7 +1755,7 @@ var options = {
 
   

Time zone

- By default, the Timeline displays time in local time. To display a Timeline in an other time zone or in UTC, the date constructor can be overloaded via the configuration option moment, which by default is the constructor function of moment.js. More information about UTC with moment.js can be found in the docs: http://momentjs.com/docs/#/parsing/utc/. + By default, the Timeline displays time in local time. To display a Timeline in another time zone or in UTC, the date constructor can be overloaded via the configuration option moment, which by default is the constructor function of moment.js. More information about UTC with moment.js can be found in the docs: http://momentjs.com/docs/#/parsing/utc/.

@@ -1810,7 +1829,7 @@ var options = { Daysvis-date1, vis-date2, ..., vis-date31 - Monthsvis-januari, vis-februari, vis-march, vis-april, vis-may, vis-june, vis-july, vis-august, vis-september, vis-october, vis-november, vis-december + Monthsvis-january, vis-february, vis-march, vis-april, vis-may, vis-june, vis-july, vis-august, vis-september, vis-october, vis-november, vis-december Yearsvis-year2014, vis-year2015, ... diff --git a/examples/graph3d/11_tooltips.html b/examples/graph3d/11_tooltips.html index 39af19aa..c37b35f4 100644 --- a/examples/graph3d/11_tooltips.html +++ b/examples/graph3d/11_tooltips.html @@ -5,6 +5,12 @@ @@ -24,11 +30,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', - ]; + 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 @@ -42,7 +48,7 @@ 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, extra: extra_content[(x*y) % extra_content.length]}); + data.add({x:x, y:y, z: z, extra: extra_content[(x*y) % extra_content.length]}); } } } @@ -61,8 +67,8 @@ //tooltip: true, tooltip: function (point) { // 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; + // data is the original object passed to the point constructor + return 'value: ' + point.z + '
' + point.data.extra; }, keepAspectRatio: true, @@ -106,6 +112,7 @@

-
+
Hover the mouse cursor over the graph to see tooltips.
+ diff --git a/examples/timeline/items/pointItems.html b/examples/timeline/items/pointItems.html index 810a3144..68201801 100755 --- a/examples/timeline/items/pointItems.html +++ b/examples/timeline/items/pointItems.html @@ -49,7 +49,7 @@ var options = { // Set global item type. Type can also be specified for items individually - // Available types: 'box' (default), 'point', 'range', 'rangeoverflow' + // Available types: 'box' (default), 'point', 'range' type: 'point', showMajorLabels: false }; diff --git a/examples/timeline/other/horizontalScroll.html b/examples/timeline/other/horizontalScroll.html new file mode 100644 index 00000000..a999cd51 --- /dev/null +++ b/examples/timeline/other/horizontalScroll.html @@ -0,0 +1,77 @@ + + + Timeline | Horizontal Scroll Option + + + + + + + + + +

Timeline horizontal scroll option

+ +
+ + + + + diff --git a/examples/timeline/other/usingReact.html b/examples/timeline/other/usingReact.html new file mode 100644 index 00000000..f6d1e1f7 --- /dev/null +++ b/examples/timeline/other/usingReact.html @@ -0,0 +1,123 @@ + + + + + React Components in templates + + + +
+ + + + + + + + + + + + + diff --git a/examples/timeline/other/verticalScroll.html b/examples/timeline/other/verticalScroll.html new file mode 100644 index 00000000..07a68ad4 --- /dev/null +++ b/examples/timeline/other/verticalScroll.html @@ -0,0 +1,92 @@ + + + Timeline | Vertical Scroll Option + + + + + + + + + +

Timeline vertical scroll option

+ +

With +verticalScroll: true, +zoomKey: 'ctrlKey' +

+
+ +

With +horizontalScroll: true, +verticalScroll: true, +zoomKey: 'ctrlKey' +

+
+ + + + diff --git a/lib/DataSet.js b/lib/DataSet.js index fbc6b92a..8f3b910f 100644 --- a/lib/DataSet.js +++ b/lib/DataSet.js @@ -668,25 +668,26 @@ DataSet.prototype._sort = function (items, order) { */ DataSet.prototype.remove = function (id, senderId) { var removedIds = [], - i, len, removedId; - - if (Array.isArray(id)) { - for (i = 0, len = id.length; i < len; i++) { - removedId = this._remove(id[i]); - if (removedId != null) { - removedIds.push(removedId); + removedItems = [], + ids = [], + i, len, itemId, item; + + // force everything to be an array for simplicity + ids = Array.isArray(id) ? id : [id]; + + for (i = 0, len = ids.length; i < len; i++) { + item = this._remove(ids[i]); + if (item) { + itemId = item[this._fieldId]; + if (itemId) { + removedIds.push(itemId); + removedItems.push(item); } } } - else { - removedId = this._remove(id); - if (removedId != null) { - removedIds.push(removedId); - } - } if (removedIds.length) { - this._trigger('remove', {items: removedIds}, senderId); + this._trigger('remove', {items: removedIds, oldData: removedItems}, senderId); } return removedIds; @@ -699,20 +700,23 @@ DataSet.prototype.remove = function (id, senderId) { * @private */ DataSet.prototype._remove = function (id) { + var item, + ident; + + // confirm the id to use based on the args type if (util.isNumber(id) || util.isString(id)) { - if (this._data[id]) { - delete this._data[id]; - this.length--; - return id; - } + ident = id; } else if (id instanceof Object) { - var itemId = id[this._fieldId]; - if (itemId !== undefined && this._data[itemId]) { - delete this._data[itemId]; - this.length--; - return itemId; - } + ident = id[this._fieldId]; // look for the identifier field using _fieldId + } + + // do the remove if the item is found + if (ident !== undefined && this._data[ident]) { + item = this._data[ident]; + delete this._data[ident]; + this.length--; + return item; } return null; }; @@ -723,12 +727,18 @@ DataSet.prototype._remove = function (id) { * @return {Array} removedIds The ids of all removed items */ DataSet.prototype.clear = function (senderId) { + var i, len; var ids = Object.keys(this._data); + var items = []; + + for (i = 0, len = ids.length; i < len; i++) { + items.push(this._data[ids[i]]); + } this._data = {}; this.length = 0; - this._trigger('remove', {items: ids}, senderId); + this._trigger('remove', {items: ids, oldData: items}, senderId); return ids; }; diff --git a/lib/DataView.js b/lib/DataView.js index ba7a864e..5a844cff 100644 --- a/lib/DataView.js +++ b/lib/DataView.js @@ -35,7 +35,7 @@ function DataView (data, options) { * @param {DataSet | DataView} data */ DataView.prototype.setData = function (data) { - var ids, id, i, len; + var ids, id, i, len, items; if (this._data) { // unsubscribe from current dataset @@ -44,10 +44,16 @@ DataView.prototype.setData = function (data) { } // trigger a remove of all items in memory - ids = Object.keys(this._ids); + ids = this._data.getIds({filter: this._options && this._options.filter}); + items = []; + + for (i = 0, len = ids.length; i < len; i++) { + items.push(this._data._data[ids[i]]); + } + this._ids = {}; this.length = 0; - this._trigger('remove', {items: ids}); + this._trigger('remove', {items: ids, oldData: items}); } this._data = data; @@ -80,18 +86,19 @@ DataView.prototype.setData = function (data) { */ DataView.prototype.refresh = function () { var id, i, len; - var ids = this._data.getIds({filter: this._options && this._options.filter}); - var oldIds = Object.keys(this._ids); - var newIds = {}; - var added = []; - var removed = []; + var ids = this._data.getIds({filter: this._options && this._options.filter}), + oldIds = Object.keys(this._ids), + newIds = {}, + addedIds = [], + removedIds = [], + removedItems = []; // check for additions for (i = 0, len = ids.length; i < len; i++) { id = ids[i]; newIds[id] = true; if (!this._ids[id]) { - added.push(id); + addedIds.push(id); this._ids[id] = true; } } @@ -100,19 +107,20 @@ DataView.prototype.refresh = function () { for (i = 0, len = oldIds.length; i < len; i++) { id = oldIds[i]; if (!newIds[id]) { - removed.push(id); + removedIds.push(id); + removedItems.push(this._data[id]); delete this._ids[id]; } } - this.length += added.length - removed.length; + this.length += addedIds.length - removedIds.length; // trigger events - if (added.length) { + if (addedIds.length) { this._trigger('add', {items: added}); } - if (removed.length) { - this._trigger('remove', {items: removed}); + if (removedIds.length) { + this._trigger('remove', {items: removedIds, oldData: removedItems}); } }; @@ -298,14 +306,14 @@ DataView.prototype.getDataSet = function () { DataView.prototype._onEvent = function (event, params, senderId) { var i, len, id, item; var ids = params && params.items; - var data = this._data; - var updatedData = []; - var oldData = []; - var added = []; - var updated = []; - var removed = []; - - if (ids && data) { + var addedIds = [], + updatedIds = [], + removedIds = [], + oldItems = [], + updatedItems = [], + removedItems = []; + + if (ids && this._data) { switch (event) { case 'add': // filter the ids of the added items @@ -314,7 +322,7 @@ DataView.prototype._onEvent = function (event, params, senderId) { item = this.get(id); if (item) { this._ids[id] = true; - added.push(id); + addedIds.push(id); } } @@ -329,19 +337,20 @@ DataView.prototype._onEvent = function (event, params, senderId) { if (item) { if (this._ids[id]) { - updated.push(id); - updatedData.push(params.data[i]); - oldData.push(params.oldData[i]); + updatedIds.push(id); + updatedItems.push(params.data[i]); + oldItems.push(params.oldData[i]); } else { this._ids[id] = true; - added.push(id); + addedIds.push(id); } } else { if (this._ids[id]) { delete this._ids[id]; - removed.push(id); + removedIds.push(id); + removedItems.push(params.oldData[i]); } else { // nothing interesting for me :-( @@ -357,23 +366,24 @@ DataView.prototype._onEvent = function (event, params, senderId) { id = ids[i]; if (this._ids[id]) { delete this._ids[id]; - removed.push(id); + removedIds.push(id); + removedItems.push(params.oldData[i]); } } break; } - this.length += added.length - removed.length; + this.length += addedIds.length - removedIds.length; - if (added.length) { - this._trigger('add', {items: added}, senderId); + if (addedIds.length) { + this._trigger('add', {items: addedIds}, senderId); } - if (updated.length) { - this._trigger('update', {items: updated, oldData: oldData, data: updatedData}, senderId); + if (updatedIds.length) { + this._trigger('update', {items: updatedIds, oldData: oldItems, data: updatedItems}, senderId); } - if (removed.length) { - this._trigger('remove', {items: removed}, senderId); + if (removedIds.length) { + this._trigger('remove', {items: removedIds, oldData: removedItems}, senderId); } } }; diff --git a/lib/graph3d/Graph3d.js b/lib/graph3d/Graph3d.js index 102312a9..10e5f5e8 100644 --- a/lib/graph3d/Graph3d.js +++ b/lib/graph3d/Graph3d.js @@ -7,80 +7,22 @@ var Camera = require('./Camera'); var Filter = require('./Filter'); var Slider = require('./Slider'); var StepNumber = require('./StepNumber'); +var Settings = require('./Settings'); -// ----------------------------------------------------------------------------- -// 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 -}; +Graph3d.STYLE = Settings.STYLE; /** - * 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' -]; + * Following label is used in the settings to describe values which + * should be determined by the code while running, from the current + * data and graph style. + * + * Using 'undefined' directly achieves the same thing, but this is + * more descriptive by describing the intent. + */ +var autoByDefault = undefined; /** @@ -89,15 +31,8 @@ var PREFIXEDOPTIONKEYS = [ * 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'. + * If a field is not in this list, a default value of 'autoByDefault' + * is assumed, which is just an alias for 'undefined'. */ var DEFAULTS = { width : '400px', @@ -114,29 +49,28 @@ var DEFAULTS = { showPerspective : true, showShadow : false, keepAspectRatio : true, - verticalRatio : 0.5, // 0.1 to 1.0, where 1.0 results in a 'cube' + verticalRatio : 0.5, // 0.1 to 1.0, where 1.0 results in a 'cube' + dotSizeRatio : 0.02, // size of the dots as a fraction of the graph width - showAnimationControls: undefined, // auto by default - animationInterval : 1000, // milliseconds + showAnimationControls: autoByDefault, + animationInterval : 1000, // milliseconds animationPreload : false, - animationAutoStart : undefined, // auto by default + animationAutoStart : autoByDefault, 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, + showLegend : autoByDefault, // determined by graph style + backgroundColor : autoByDefault, dataColor : { fill : '#7DC1FF', stroke : '#3267D2', - strokeWidth: 1 // px + strokeWidth: 1 // px }, cameraPosition : { @@ -145,103 +79,22 @@ var DEFAULTS = { 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 + xBarWidth : autoByDefault, + yBarWidth : autoByDefault, + valueMin : autoByDefault, + valueMax : autoByDefault, + xMin : autoByDefault, + xMax : autoByDefault, + xStep : autoByDefault, + yMin : autoByDefault, + yMax : autoByDefault, + yStep : autoByDefault, + zMin : autoByDefault, + zMax : autoByDefault, + zStep : autoByDefault }; -/** - * 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 // ----------------------------------------------------------------------------- @@ -272,27 +125,7 @@ function Graph3d(container, data, options) { // 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? - - // Handle the more complex ('special') fields - this._setSpecialSettings(DEFAULTS, this); - - // - // End Set Defaults - // + Settings.setDefaults(DEFAULTS, this); // the column indexes this.colX = undefined; @@ -370,21 +203,23 @@ Graph3d.prototype._convert3Dto2D = function(point3d) { * camera */ Graph3d.prototype._convertPointToTranslation = function(point3d) { - var ax = point3d.x * this.scale.x, + var cameraLocation = this.camera.getCameraLocation(), + cameraRotation = this.camera.getCameraRotation(), + ax = point3d.x * this.scale.x, ay = point3d.y * this.scale.y, az = point3d.z * this.scale.z, - cx = this.camera.getCameraLocation().x, - cy = this.camera.getCameraLocation().y, - cz = this.camera.getCameraLocation().z, + cx = cameraLocation.x, + cy = cameraLocation.y, + cz = cameraLocation.z, // calculate angles - sinTx = Math.sin(this.camera.getCameraRotation().x), - cosTx = Math.cos(this.camera.getCameraRotation().x), - sinTy = Math.sin(this.camera.getCameraRotation().y), - cosTy = Math.cos(this.camera.getCameraRotation().y), - sinTz = Math.sin(this.camera.getCameraRotation().z), - cosTz = Math.cos(this.camera.getCameraRotation().z), + sinTx = Math.sin(cameraRotation.x), + cosTx = Math.cos(cameraRotation.x), + sinTy = Math.sin(cameraRotation.y), + cosTy = Math.cos(cameraRotation.y), + sinTz = Math.sin(cameraRotation.z), + cosTz = Math.cos(cameraRotation.z), // calculate translation dx = cosTy * (sinTz * (ay - cy) + cosTz * (ax - cx)) - sinTy * (az - cz), @@ -459,264 +294,6 @@ Graph3d.prototype._calcTranslations = function(points, sort) { }; -// ----------------------------------------------------------------------------- -// 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, dst) { - var fill = 'white'; - var stroke = 'gray'; - var strokeWidth = 1; - - if (typeof(backgroundColor) === 'string') { - fill = backgroundColor; - stroke = 'none'; - strokeWidth = 0; - } - else if (typeof(backgroundColor) === 'object') { - if (backgroundColor.fill !== undefined) fill = backgroundColor.fill; - if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke; - if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth; - } - 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 { - 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; - } - - if (dst.camera === undefined) { - dst.camera = new Camera(); - } - - dst.camera.setArmRotation(camPos.horizontal, camPos.vertical); - dst.camera.setArmLength(camPos.distance); -}; - - -// -// 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' - * @return {Number} styleNumber Enumeration value representing the style, or -1 - * when not found - */ -Graph3d.prototype._getStyleNumber = function(styleName) { - switch (styleName) { - case 'dot': return Graph3d.STYLE.DOT; - case 'dot-line': return Graph3d.STYLE.DOTLINE; - case 'dot-color': return Graph3d.STYLE.DOTCOLOR; - case 'dot-size': return Graph3d.STYLE.DOTSIZE; - case 'line': return Graph3d.STYLE.LINE; - case 'grid': return Graph3d.STYLE.GRID; - case 'surface': return Graph3d.STYLE.SURFACE; - case 'bar': return Graph3d.STYLE.BAR; - case 'bar-color': return Graph3d.STYLE.BARCOLOR; - case 'bar-size': return Graph3d.STYLE.BARSIZE; - } - - return -1; -}; - -/** - * Determine the indexes of the data columns, based on the given style and data - * @param {DataSet} data - * @param {Number} style - */ -Graph3d.prototype._determineColumnIndexes = function(data, style) { - if (this.style === Graph3d.STYLE.DOT || - this.style === Graph3d.STYLE.DOTLINE || - this.style === Graph3d.STYLE.LINE || - this.style === Graph3d.STYLE.GRID || - this.style === Graph3d.STYLE.SURFACE || - this.style === Graph3d.STYLE.BAR) { - // 3 columns expected, and optionally a 4th with filter values - this.colX = 0; - this.colY = 1; - this.colZ = 2; - this.colValue = undefined; - - if (data.getNumberOfColumns() > 3) { - this.colFilter = 3; - } - } - else if (this.style === Graph3d.STYLE.DOTCOLOR || - this.style === Graph3d.STYLE.DOTSIZE || - this.style === Graph3d.STYLE.BARCOLOR || - this.style === Graph3d.STYLE.BARSIZE) { - // 4 columns expected, and optionally a 5th with filter values - this.colX = 0; - this.colY = 1; - this.colZ = 2; - this.colValue = 3; - - if (data.getNumberOfColumns() > 4) { - this.colFilter = 4; - } - } - else { - throw new Error('Unknown style "' + this.style + '"'); - } -}; - Graph3d.prototype.getNumberOfRows = function(data) { return data.length; } @@ -745,14 +322,57 @@ Graph3d.prototype.getDistinctValues = function(data, column) { Graph3d.prototype.getColumnRange = function(data,column) { - var minMax = {min:data[0][column],max:data[0][column]}; + var minMax; + for (var i = 0; i < data.length; i++) { - if (minMax.min > data[i][column]) { minMax.min = data[i][column]; } - if (minMax.max < data[i][column]) { minMax.max = data[i][column]; } + var item = data[i][column]; + + if (i === 0) { + minMax = { min: item, max: item}; + } else { + if (minMax.min > item) { minMax.min = item; } + if (minMax.max < item) { minMax.max = item; } + } } return minMax; }; + +/** + * Check if the state is consistent for the use of the value field. + * + * Throws if a problem is detected. + */ +Graph3d.prototype._checkValueField = function (data) { + + var hasValueField = this.style === Graph3d.STYLE.BARCOLOR + || this.style === Graph3d.STYLE.BARSIZE + || this.style === Graph3d.STYLE.DOTCOLOR + || this.style === Graph3d.STYLE.DOTSIZE; + + if (!hasValueField) { + return; // No need to check further + } + + // Following field must be present for the current graph style + if (this.colValue === undefined) { + throw new Error('Expected data to have ' + + ' field \'style\' ' + + ' for graph style \'' + this.style + '\'' + ); + } + + // The data must also contain this field. + // Note that only first data element is checked + if (data[0][this.colValue] === undefined) { + throw new Error('Expected data to have ' + + ' field \'' + this.colValue + '\' ' + + ' for graph style \'' + this.style + '\'' + ); + } +}; + + /** * Initialize the data from the data table. Calculate minimum and maximum values * and column index values @@ -794,12 +414,6 @@ Graph3d.prototype._dataInitialize = function (rawData, style) { }; this.dataSet.on('*', this._onChange); - // _determineColumnIndexes - // getNumberOfRows (points) - // getNumberOfColumns (x,y,z,v,t,t1,t2...) - // getDistinctValues (unique values?) - // getColumnRange - // determine the location of x,y,z,value,filter columns this.colX = 'x'; this.colY = 'y'; @@ -807,7 +421,8 @@ Graph3d.prototype._dataInitialize = function (rawData, style) { // 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! + // Only set this field if it's actually present + this.colFilter = 'filter'; if (this.dataFilter === undefined) { this.dataFilter = new Filter(rawData, this.colFilter, this); @@ -866,7 +481,6 @@ 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; - // 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); @@ -964,29 +578,7 @@ 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 + '\'' - ); - } - } - + this._checkValueField(data); // copy all values from the google data table to a list with Point3d objects for (i = 0; i < data.length; i++) { @@ -1006,6 +598,13 @@ Graph3d.prototype._getDataPoints = function (data) { obj.trans = undefined; obj.screen = undefined; + if (this.style === Graph3d.STYLE.LINE) { + if (i > 0) { + // Add next point for line drawing + dataPoints[i - 1].pointNext = obj; + } + } + dataPoints.push(obj); } } @@ -1203,17 +802,9 @@ Graph3d.prototype.setOptions = function (options) { this.animationStop(); - if (options !== undefined) { - // retrieve parameter values - - // Handle the parameters which can be simply copied over - safeCopy(options, this, OPTIONKEYS); - safeCopy(options, this, PREFIXEDOPTIONKEYS, 'default'); - - // Handle the more complex ('special') fields - this._setSpecialSettings(options, this); - } + Settings.setOptions(options, this); + this.setPointDrawingMethod(); this.setSize(this.width, this.height); // re-load the data @@ -1227,6 +818,54 @@ Graph3d.prototype.setOptions = function (options) { } }; + +/** + * Determine which point drawing method to use for the current graph style. + */ +Graph3d.prototype.setPointDrawingMethod = function() { + var method = undefined; + + switch (this.style) { + case Graph3d.STYLE.BAR: + method = Graph3d.prototype._redrawBarGraphPoint; + break; + case Graph3d.STYLE.BARCOLOR: + method = Graph3d.prototype._redrawBarColorGraphPoint; + break; + case Graph3d.STYLE.BARSIZE: + method = Graph3d.prototype._redrawBarSizeGraphPoint; + break; + case Graph3d.STYLE.DOT: + method = Graph3d.prototype._redrawDotGraphPoint; + break; + case Graph3d.STYLE.DOTLINE: + method = Graph3d.prototype._redrawDotLineGraphPoint; + break; + case Graph3d.STYLE.DOTCOLOR: + method = Graph3d.prototype._redrawDotColorGraphPoint; + break; + case Graph3d.STYLE.DOTSIZE: + method = Graph3d.prototype._redrawDotSizeGraphPoint; + break; + case Graph3d.STYLE.SURFACE: + method = Graph3d.prototype._redrawSurfaceGraphPoint; + break; + case Graph3d.STYLE.GRID: + method = Graph3d.prototype._redrawGridGraphPoint; + break; + case Graph3d.STYLE.LINE: + method = Graph3d.prototype._redrawLineGraphPoint; + break; + default: + throw new Error('Can not determine point drawing method ' + + 'for graph style \'' + this.style + '\''); + break; + } + + this._pointDrawingMethod = method; +}; + + /** * Redraw the Graph. */ @@ -1241,22 +880,7 @@ Graph3d.prototype.redraw = function() { this._redrawClear(); this._redrawAxis(); - if (this.style === Graph3d.STYLE.GRID || - this.style === Graph3d.STYLE.SURFACE) { - this._redrawDataGrid(); - } - else if (this.style === Graph3d.STYLE.LINE) { - this._redrawDataLine(); - } - else if (this.style === Graph3d.STYLE.BAR || - this.style === Graph3d.STYLE.BARCOLOR || - this.style === Graph3d.STYLE.BARSIZE) { - this._redrawDataBar(); - } - else { - // style is DOT, DOTLINE, DOTCOLOR, DOTSIZE - this._redrawDataDot(); - } + this._redrawDataGraph(); this._redrawInfo(); this._redrawLegend(); @@ -1269,6 +893,10 @@ Graph3d.prototype.redraw = function() { Graph3d.prototype._getContext = function() { var canvas = this.frame.canvas; var ctx = canvas.getContext('2d'); + + ctx.lineJoin = 'round'; + ctx.lineCap = 'round'; + return ctx; }; @@ -1284,13 +912,19 @@ Graph3d.prototype._redrawClear = function() { }; +Graph3d.prototype._dotSize = function() { + return this.frame.clientWidth * this.dotSizeRatio; +}; + + /** * Get legend width */ Graph3d.prototype._getLegendWidth = function() { var width; + if (this.style === Graph3d.STYLE.DOTSIZE) { - var dotSize = this.frame.clientWidth * this.dotSizeRatio; + var dotSize = this._dotSize(); width = dotSize / 2 + dotSize * 2; } else if (this.style === Graph3d.STYLE.BARSIZE) { width = this.xBarWidth ; @@ -1307,12 +941,16 @@ Graph3d.prototype._getLegendWidth = function() { Graph3d.prototype._redrawLegend = function() { //Return without drawing anything, if no legend is specified - if (this.showLegend !== true) {return;} + if (this.showLegend !== true) { + return; + } // Do not draw legend when graph style does not support if (this.style === Graph3d.STYLE.LINE || this.style === Graph3d.STYLE.BARSIZE //TODO add legend support for BARSIZE - ){return;} + ){ + return; + } // Legend types - size and color. Determine if size legend. var isSizeLegend = (this.style === Graph3d.STYLE.BARSIZE @@ -1359,7 +997,7 @@ Graph3d.prototype._redrawLegend = function() { // draw the size legend box var widthMin; if (this.style === Graph3d.STYLE.DOTSIZE) { - var dotSize = this.frame.clientWidth * this.dotSizeRatio; + var dotSize = this._dotSize(); widthMin = dotSize / 2; // px } else if (this.style === Graph3d.STYLE.BARSIZE) { //widthMin = this.xBarWidth * 0.2 this is wrong - barwidth measures in terms of xvalues @@ -1385,13 +1023,13 @@ Graph3d.prototype._redrawLegend = function() { step.start(true); var y; + var from; + var to; while (!step.end()) { y = bottom - (step.getCurrent() - legendMin) / (legendMax - legendMin) * height; - - ctx.beginPath(); - ctx.moveTo(left - gridLineLen, y); - ctx.lineTo(left, y); - ctx.stroke(); + from = new Point2d(left - gridLineLen, y); + to = new Point2d(left, y); + this._line(ctx, from, to); ctx.textAlign = 'right'; ctx.textBaseline = 'middle'; @@ -1405,7 +1043,6 @@ Graph3d.prototype._redrawLegend = function() { ctx.textBaseline = 'top'; var label = this.legendLabel; ctx.fillText(label, right, bottom + this.margin); - }; /** @@ -1771,364 +1408,442 @@ Graph3d.prototype._hsv2rgb = function(H, S, V) { }; -/** - * Draw all datapoints as a grid - * This function can be used when the style is 'grid' - */ -Graph3d.prototype._redrawDataGrid = function() { - var ctx = this._getContext(), - point, right, top, cross, - i, - topSideVisible, fillStyle, strokeStyle, lineWidth, - h, s, v, zAvg; - - ctx.lineJoin = 'round'; - ctx.lineCap = 'round'; +Graph3d.prototype._getStrokeWidth = function(point) { + if (point !== undefined) { + if (this.showPerspective) { + return 1 / -point.trans.z * this.dataColor.strokeWidth; + } + else { + return -(this.eye.z / this.camera.getArmLength()) * this.dataColor.strokeWidth; + } + } - if (this.dataPoints === undefined || this.dataPoints.length <= 0) - return; // TODO: throw exception? + return this.dataColor.strokeWidth; +}; - this._calcTranslations(this.dataPoints); - if (this.style === Graph3d.STYLE.SURFACE) { - for (i = 0; i < this.dataPoints.length; i++) { - point = this.dataPoints[i]; - right = this.dataPoints[i].pointRight; - top = this.dataPoints[i].pointTop; - cross = this.dataPoints[i].pointCross; - - if (point !== undefined && right !== undefined && top !== undefined && cross !== undefined) { - - if (this.showGrayBottom || this.showShadow) { - // calculate the cross product of the two vectors from center - // to left and right, in order to know whether we are looking at the - // bottom or at the top side. We can also use the cross product - // for calculating light intensity - var aDiff = Point3d.subtract(cross.trans, point.trans); - var bDiff = Point3d.subtract(top.trans, right.trans); - var crossproduct = Point3d.crossProduct(aDiff, bDiff); - var len = crossproduct.length(); - // FIXME: there is a bug with determining the surface side (shadow or colored) - - topSideVisible = (crossproduct.z > 0); - } - else { - topSideVisible = true; - } +// ----------------------------------------------------------------------------- +// Drawing primitives for the graphs +// ----------------------------------------------------------------------------- - if (topSideVisible) { - // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 - zAvg = (point.point.z + right.point.z + top.point.z + cross.point.z) / 4; - h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; - s = 1; // saturation - if (this.showShadow) { - v = Math.min(1 + (crossproduct.x / len) / 2, 1); // value. TODO: scale - fillStyle = this._hsv2rgb(h, s, v); - strokeStyle = fillStyle; - } - else { - v = 1; - fillStyle = this._hsv2rgb(h, s, v); - strokeStyle = this.axisColor; // TODO: should be customizable - } - } - else { - fillStyle = 'gray'; - strokeStyle = this.axisColor; - } +/** + * Draw a bar element in the view with the given properties. + */ +Graph3d.prototype._redrawBar = function(ctx, point, xWidth, yWidth, color, borderColor) { + var i, j, surface; - ctx.lineWidth = this._getStrokeWidth(point); - ctx.fillStyle = fillStyle; - ctx.strokeStyle = strokeStyle; - ctx.beginPath(); - ctx.moveTo(point.screen.x, point.screen.y); - ctx.lineTo(right.screen.x, right.screen.y); - ctx.lineTo(cross.screen.x, cross.screen.y); - ctx.lineTo(top.screen.x, top.screen.y); - ctx.closePath(); - ctx.fill(); - ctx.stroke(); // TODO: only draw stroke when strokeWidth > 0 - } - } + // calculate all corner points + var me = this; + var point3d = point.point; + var top = [ + {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, point3d.z)}, + {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, point3d.z)}, + {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, point3d.z)}, + {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, point3d.z)} + ]; + var bottom = [ + {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, this.zMin)}, + {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, this.zMin)}, + {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, this.zMin)}, + {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, this.zMin)} + ]; + + // calculate screen location of the points + top.forEach(function (obj) { + obj.screen = me._convert3Dto2D(obj.point); + }); + bottom.forEach(function (obj) { + obj.screen = me._convert3Dto2D(obj.point); + }); + + // create five sides, calculate both corner points and center points + var surfaces = [ + {corners: top, center: Point3d.avg(bottom[0].point, bottom[2].point)}, + {corners: [top[0], top[1], bottom[1], bottom[0]], center: Point3d.avg(bottom[1].point, bottom[0].point)}, + {corners: [top[1], top[2], bottom[2], bottom[1]], center: Point3d.avg(bottom[2].point, bottom[1].point)}, + {corners: [top[2], top[3], bottom[3], bottom[2]], center: Point3d.avg(bottom[3].point, bottom[2].point)}, + {corners: [top[3], top[0], bottom[0], bottom[3]], center: Point3d.avg(bottom[0].point, bottom[3].point)} + ]; + point.surfaces = surfaces; + + // calculate the distance of each of the surface centers to the camera + for (j = 0; j < surfaces.length; j++) { + surface = surfaces[j]; + var transCenter = this._convertPointToTranslation(surface.center); + surface.dist = this.showPerspective ? transCenter.length() : -transCenter.z; + // TODO: this dept calculation doesn't work 100% of the cases due to perspective, + // but the current solution is fast/simple and works in 99.9% of all cases + // the issue is visible in example 14, with graph.setCameraPosition({horizontal: 2.97, vertical: 0.5, distance: 0.9}) + } + + // order the surfaces by their (translated) depth + surfaces.sort(function (a, b) { + var diff = b.dist - a.dist; + if (diff) return diff; + + // if equal depth, sort the top surface last + if (a.corners === top) return 1; + if (b.corners === top) return -1; + + // both are equal + return 0; + }); + + // draw the ordered surfaces + ctx.lineWidth = this._getStrokeWidth(point); + ctx.strokeStyle = borderColor; + ctx.fillStyle = color; + // NOTE: we start at j=2 instead of j=0 as we don't need to draw the two surfaces at the backside + for (j = 2; j < surfaces.length; j++) { + surface = surfaces[j]; + this._polygon(ctx, surface.corners); } - else { // grid style - for (i = 0; i < this.dataPoints.length; i++) { - point = this.dataPoints[i]; - right = this.dataPoints[i].pointRight; - top = this.dataPoints[i].pointTop; - - if (point !== undefined && right !== undefined) { - // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 - zAvg = (point.point.z + right.point.z) / 2; - h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; - - ctx.lineWidth = this._getStrokeWidth(point) * 2; - ctx.strokeStyle = this._hsv2rgb(h, 1, 1); - this._line(ctx, point.screen, right.screen); - } +}; - if (point !== undefined && top !== undefined) { - // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 - zAvg = (point.point.z + top.point.z) / 2; - h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; - ctx.lineWidth = this._getStrokeWidth(point) * 2; - ctx.strokeStyle = this._hsv2rgb(h, 1, 1); - this._line(ctx, point.screen, top.screen); - } - } +/** + * Draw a polygon using the passed points and fill it with the passed style and stroke. + * + * @param points an array of points. + * @param fillStyle optional; the fill style to set + * @param strokeStyle optional; the stroke style to set + */ +Graph3d.prototype._polygon = function(ctx, points, fillStyle, strokeStyle) { + if (points.length < 2) { + return; } -}; -Graph3d.prototype._getStrokeWidth = function(point) { - if (point !== undefined) { - if (this.showPerspective) { - return 1 / -point.trans.z * this.dataColor.strokeWidth; - } - else { - return -(this.eye.z / this.camera.getArmLength()) * this.dataColor.strokeWidth; - } + if (fillStyle !== undefined) { + ctx.fillStyle = fillStyle; + } + if (strokeStyle !== undefined) { + ctx.strokeStyle = strokeStyle; } + ctx.beginPath(); + ctx.moveTo(points[0].screen.x, points[0].screen.y); - return this.dataColor.strokeWidth; + for (var i = 1; i < points.length; ++i) { + var point = points[i]; + ctx.lineTo(point.screen.x, point.screen.y); + } + + ctx.closePath(); + ctx.fill(); + ctx.stroke(); // TODO: only draw stroke when strokeWidth > 0 }; + /** - * Draw all datapoints as dots. - * This function can be used when the style is 'dot' or 'dot-line' + * @param size optional; if not specified use value from 'this._dotSize()` */ -Graph3d.prototype._redrawDataDot = function() { - var ctx = this._getContext(); - var i; +Graph3d.prototype._drawCircle = function(ctx, point, color, borderColor, size) { + var radius = this._calcRadius(point, size); - if (this.dataPoints === undefined || this.dataPoints.length <= 0) - return; // TODO: throw exception? + ctx.lineWidth = this._getStrokeWidth(point); + ctx.strokeStyle = borderColor; + ctx.fillStyle = color; + ctx.beginPath(); + ctx.arc(point.screen.x, point.screen.y, radius, 0, Math.PI*2, true); + ctx.fill(); + ctx.stroke(); +}; - this._calcTranslations(this.dataPoints); - // draw the datapoints as colored circles - var dotSize = this.frame.clientWidth * this.dotSizeRatio; // px - for (i = 0; i < this.dataPoints.length; i++) { - var point = this.dataPoints[i]; +/** + * Determine the colors for the 'regular' graph styles. + */ +Graph3d.prototype._getColorsRegular = function(point) { + // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 + var hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240; + var color = this._hsv2rgb(hue, 1, 1); + var borderColor = this._hsv2rgb(hue, 1, 0.8); + + return { + fill : color, + border: borderColor + }; +}; - if (this.style === Graph3d.STYLE.DOTLINE) { - // draw a vertical line from the bottom to the graph value - //var from = this._convert3Dto2D(new Point3d(point.point.x, point.point.y, this.zMin)); - var from = this._convert3Dto2D(point.bottom); - ctx.lineWidth = 1; - this._line(ctx, from, point.screen, this.gridColor); - } - // calculate radius for the circle - var size; - if (this.style === Graph3d.STYLE.DOTSIZE) { - size = dotSize/2 + 2*dotSize * (point.point.value - this.valueMin) / (this.valueMax - this.valueMin); - } - else { - size = dotSize; - } +/** + * Get the colors for the 'color' graph styles. + * These styles are currently: 'bar-color' and 'dot-color' + */ +Graph3d.prototype._getColorsColor = function(point) { + // calculate the color based on the value + var hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240; + var color = this._hsv2rgb(hue, 1, 1); + var borderColor = this._hsv2rgb(hue, 1, 0.8); + + return { + fill : color, + border : borderColor + }; +}; - var radius; - if (this.showPerspective) { - radius = size / -point.trans.z; - } - else { - radius = size * -(this.eye.z / this.camera.getArmLength()); - } - if (radius < 0) { - radius = 0; - } - var hue, color, borderColor; - if (this.style === Graph3d.STYLE.DOTCOLOR ) { - // calculate the color based on the value - hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240; - color = this._hsv2rgb(hue, 1, 1); - borderColor = this._hsv2rgb(hue, 1, 0.8); - } - else if (this.style === Graph3d.STYLE.DOTSIZE) { - color = this.dataColor.fill; - borderColor = this.dataColor.stroke; - } - else { - // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 - hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240; - color = this._hsv2rgb(hue, 1, 1); - borderColor = this._hsv2rgb(hue, 1, 0.8); - } +/** + * Get the colors for the 'size' graph styles. + * These styles are currently: 'bar-size' and 'dot-size' + */ +Graph3d.prototype._getColorsSize = function() { + return { + fill : this.dataColor.fill, + border : this.dataColor.stroke + }; +}; - // draw the circle - ctx.lineWidth = this._getStrokeWidth(point); - ctx.strokeStyle = borderColor; - ctx.fillStyle = color; - ctx.beginPath(); - ctx.arc(point.screen.x, point.screen.y, radius, 0, Math.PI*2, true); - ctx.fill(); - ctx.stroke(); + +/** + * Determine the size of a point on-screen, as determined by the + * distance to the camera. + * + * @param size the size that needs to be translated to screen coordinates. + * optional; if not passed, use the default point size. + */ +Graph3d.prototype._calcRadius = function(point, size) { + if (size === undefined) { + size = this._dotSize(); + } + + var radius; + if (this.showPerspective) { + radius = size / -point.trans.z; + } + else { + radius = size * -(this.eye.z / this.camera.getArmLength()); + } + if (radius < 0) { + radius = 0; } + + return radius; }; + +// ----------------------------------------------------------------------------- +// Methods for drawing points per graph style. +// ----------------------------------------------------------------------------- + + /** - * Draw all datapoints as bars. - * This function can be used when the style is 'bar', 'bar-color', or 'bar-size' + * Draw single datapoint for graph style 'bar'. */ -Graph3d.prototype._redrawDataBar = function() { - var ctx = this._getContext(); - var i, j, surface, corners; - - if (this.dataPoints === undefined || this.dataPoints.length <= 0) - return; // TODO: throw exception? +Graph3d.prototype._redrawBarGraphPoint = function(ctx, point) { + var xWidth = this.xBarWidth / 2; + var yWidth = this.yBarWidth / 2; + var colors = this._getColorsRegular(point); - this._calcTranslations(this.dataPoints); + this._redrawBar(ctx, point, xWidth, yWidth, colors.fill, colors.border); +}; - ctx.lineJoin = 'round'; - ctx.lineCap = 'round'; - // draw the datapoints as bars +/** + * Draw single datapoint for graph style 'bar-color'. + */ +Graph3d.prototype._redrawBarColorGraphPoint = function(ctx, point) { var xWidth = this.xBarWidth / 2; var yWidth = this.yBarWidth / 2; - for (i = 0; i < this.dataPoints.length; i++) { - var point = this.dataPoints[i]; + var colors = this._getColorsColor(point); - // determine color - var hue, color, borderColor; - if (this.style === Graph3d.STYLE.BARCOLOR ) { - // calculate the color based on the value - hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240; - color = this._hsv2rgb(hue, 1, 1); - borderColor = this._hsv2rgb(hue, 1, 0.8); - } - else if (this.style === Graph3d.STYLE.BARSIZE) { - color = this.dataColor.fill; - borderColor = this.dataColor.stroke; + this._redrawBar(ctx, point, xWidth, yWidth, colors.fill, colors.border); +}; + + +/** + * Draw single datapoint for graph style 'bar-size'. + */ +Graph3d.prototype._redrawBarSizeGraphPoint = function(ctx, point) { + // calculate size for the bar + var fraction = (point.point.value - this.valueMin) / (this.valueMax - this.valueMin); + var xWidth = (this.xBarWidth / 2) * (fraction * 0.8 + 0.2); + var yWidth = (this.yBarWidth / 2) * (fraction * 0.8 + 0.2); + + var colors = this._getColorsSize(); + + this._redrawBar(ctx, point, xWidth, yWidth, colors.fill, colors.border); +}; + + +/** + * Draw single datapoint for graph style 'dot'. + */ +Graph3d.prototype._redrawDotGraphPoint = function(ctx, point) { + var colors = this._getColorsRegular(point); + + this._drawCircle(ctx, point, colors.fill, colors.border); +}; + + +/** + * Draw single datapoint for graph style 'dot-line'. + */ +Graph3d.prototype._redrawDotLineGraphPoint = function(ctx, point) { + // draw a vertical line from the XY-plane to the graph value + var from = this._convert3Dto2D(point.bottom); + ctx.lineWidth = 1; + this._line(ctx, from, point.screen, this.gridColor); + + this._redrawDotGraphPoint(ctx, point); +}; + + +/** + * Draw single datapoint for graph style 'dot-color'. + */ +Graph3d.prototype._redrawDotColorGraphPoint = function(ctx, point) { + var colors = this._getColorsColor(point); + + this._drawCircle(ctx, point, colors.fill, colors.border); +}; + + +/** + * Draw single datapoint for graph style 'dot-size'. + */ +Graph3d.prototype._redrawDotSizeGraphPoint = function(ctx, point) { + var dotSize = this._dotSize(); + var fraction = (point.point.value - this.valueMin) / (this.valueMax - this.valueMin); + var size = dotSize/2 + 2*dotSize * fraction; + var colors = this._getColorsSize(); + + this._drawCircle(ctx, point, colors.fill, colors.border, size); +}; + + +/** + * Draw single datapoint for graph style 'surface'. + */ +Graph3d.prototype._redrawSurfaceGraphPoint = function(ctx, point) { + var right = point.pointRight; + var top = point.pointTop; + var cross = point.pointCross; + + if (point === undefined || right === undefined || top === undefined || cross === undefined) { + return; + } + + var topSideVisible = true; + var fillStyle; + var strokeStyle; + var lineWidth; + + if (this.showGrayBottom || this.showShadow) { + // calculate the cross product of the two vectors from center + // to left and right, in order to know whether we are looking at the + // bottom or at the top side. We can also use the cross product + // for calculating light intensity + var aDiff = Point3d.subtract(cross.trans, point.trans); + var bDiff = Point3d.subtract(top.trans, right.trans); + var crossproduct = Point3d.crossProduct(aDiff, bDiff); + var len = crossproduct.length(); + // FIXME: there is a bug with determining the surface side (shadow or colored) + + topSideVisible = (crossproduct.z > 0); + } + + if (topSideVisible) { + + // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 + var zAvg = (point.point.z + right.point.z + top.point.z + cross.point.z) / 4; + var h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; + var s = 1; // saturation + var v; + + if (this.showShadow) { + v = Math.min(1 + (crossproduct.x / len) / 2, 1); // value. TODO: scale + fillStyle = this._hsv2rgb(h, s, v); + strokeStyle = fillStyle; } - else { - // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 - hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240; - color = this._hsv2rgb(hue, 1, 1); - borderColor = this._hsv2rgb(hue, 1, 0.8); + else { + v = 1; + fillStyle = this._hsv2rgb(h, s, v); + strokeStyle = this.axisColor; // TODO: should be customizable } + } + else { + fillStyle = 'gray'; + strokeStyle = this.axisColor; + } - // calculate size for the bar - if (this.style === Graph3d.STYLE.BARSIZE) { - xWidth = (this.xBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2); - yWidth = (this.yBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2); - } + ctx.lineWidth = this._getStrokeWidth(point); + // TODO: only draw stroke when strokeWidth > 0 - // calculate all corner points - var me = this; - var point3d = point.point; - var top = [ - {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, point3d.z)}, - {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, point3d.z)}, - {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, point3d.z)}, - {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, point3d.z)} - ]; - var bottom = [ - {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, this.zMin)}, - {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, this.zMin)}, - {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, this.zMin)}, - {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, this.zMin)} - ]; - - // calculate screen location of the points - top.forEach(function (obj) { - obj.screen = me._convert3Dto2D(obj.point); - }); - bottom.forEach(function (obj) { - obj.screen = me._convert3Dto2D(obj.point); - }); - - // create five sides, calculate both corner points and center points - var surfaces = [ - {corners: top, center: Point3d.avg(bottom[0].point, bottom[2].point)}, - {corners: [top[0], top[1], bottom[1], bottom[0]], center: Point3d.avg(bottom[1].point, bottom[0].point)}, - {corners: [top[1], top[2], bottom[2], bottom[1]], center: Point3d.avg(bottom[2].point, bottom[1].point)}, - {corners: [top[2], top[3], bottom[3], bottom[2]], center: Point3d.avg(bottom[3].point, bottom[2].point)}, - {corners: [top[3], top[0], bottom[0], bottom[3]], center: Point3d.avg(bottom[0].point, bottom[3].point)} - ]; - point.surfaces = surfaces; - - // calculate the distance of each of the surface centers to the camera - for (j = 0; j < surfaces.length; j++) { - surface = surfaces[j]; - var transCenter = this._convertPointToTranslation(surface.center); - surface.dist = this.showPerspective ? transCenter.length() : -transCenter.z; - // TODO: this dept calculation doesn't work 100% of the cases due to perspective, - // but the current solution is fast/simple and works in 99.9% of all cases - // the issue is visible in example 14, with graph.setCameraPosition({horizontal: 2.97, vertical: 0.5, distance: 0.9}) - } + var points = [point, right, cross, top]; + this._polygon(ctx, points, fillStyle, strokeStyle); +}; - // order the surfaces by their (translated) depth - surfaces.sort(function (a, b) { - var diff = b.dist - a.dist; - if (diff) return diff; - - // if equal depth, sort the top surface last - if (a.corners === top) return 1; - if (b.corners === top) return -1; - - // both are equal - return 0; - }); - - // draw the ordered surfaces - ctx.lineWidth = this._getStrokeWidth(point); - ctx.strokeStyle = borderColor; - ctx.fillStyle = color; - // NOTE: we start at j=2 instead of j=0 as we don't need to draw the two surfaces at the backside - for (j = 2; j < surfaces.length; j++) { - surface = surfaces[j]; - corners = surface.corners; - ctx.beginPath(); - ctx.moveTo(corners[3].screen.x, corners[3].screen.y); - ctx.lineTo(corners[0].screen.x, corners[0].screen.y); - ctx.lineTo(corners[1].screen.x, corners[1].screen.y); - ctx.lineTo(corners[2].screen.x, corners[2].screen.y); - ctx.lineTo(corners[3].screen.x, corners[3].screen.y); - ctx.fill(); - ctx.stroke(); - } + +/** + * Helper method for _redrawGridGraphPoint() + */ +Graph3d.prototype._drawGridLine = function(ctx, from, to) { + if (from === undefined || to === undefined) { + return; } + + // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 + var zAvg = (from.point.z + to.point.z) / 2; + var h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; + + ctx.lineWidth = this._getStrokeWidth(from) * 2; + ctx.strokeStyle = this._hsv2rgb(h, 1, 1); + this._line(ctx, from.screen, to.screen); }; /** - * Draw a line through all datapoints. - * This function can be used when the style is 'line' + * Draw single datapoint for graph style 'Grid'. */ -Graph3d.prototype._redrawDataLine = function() { - var ctx = this._getContext(), - point, i; +Graph3d.prototype._redrawGridGraphPoint = function(ctx, point) { + this._drawGridLine(ctx, point, point.pointRight); + this._drawGridLine(ctx, point, point.pointTop); +}; - if (this.dataPoints === undefined || this.dataPoints.length <= 0) - return; // TODO: throw exception? - this._calcTranslations(this.dataPoints, false); +/** + * Draw single datapoint for graph style 'line'. + */ +Graph3d.prototype._redrawLineGraphPoint = function(ctx, point) { + if (point.pointNext === undefined) { + return; + } - // start the line - if (this.dataPoints.length > 0) { - point = this.dataPoints[0]; + ctx.lineWidth = this._getStrokeWidth(point); + ctx.strokeStyle = this.dataColor.stroke; - ctx.lineWidth = this._getStrokeWidth(point); - ctx.lineJoin = 'round'; - ctx.lineCap = 'round'; - ctx.strokeStyle = this.dataColor.stroke; - ctx.beginPath(); - ctx.moveTo(point.screen.x, point.screen.y); + this._line(ctx, point.screen, point.pointNext.screen); +}; - // draw the datapoints as colored circles - for (i = 1; i < this.dataPoints.length; i++) { - point = this.dataPoints[i]; - ctx.lineTo(point.screen.x, point.screen.y); - } - // finish the line - ctx.stroke(); +/** + * Draw all datapoints for currently selected graph style. + * + */ +Graph3d.prototype._redrawDataGraph = function() { + var ctx = this._getContext(); + var i; + + if (this.dataPoints === undefined || this.dataPoints.length <= 0) + return; // TODO: throw exception? + + this._calcTranslations(this.dataPoints); + + for (i = 0; i < this.dataPoints.length; i++) { + var point = this.dataPoints[i]; + + // Using call() ensures that the correct context is used + this._pointDrawingMethod.call(this, ctx, point); } }; + +// ----------------------------------------------------------------------------- +// End methods for drawing points per graph style. +// ----------------------------------------------------------------------------- + + /** * Start a moving operation inside the provided parent element * @param {Event} event The event that occurred (required for @@ -2571,4 +2286,36 @@ function getMouseY (event) { return event.targetTouches[0] && event.targetTouches[0].clientY || 0; } + +// ----------------------------------------------------------------------------- +// 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) { + Settings.setCameraPosition(pos, this); + this.redraw(); +}; + + +// ----------------------------------------------------------------------------- +// End public methods for specific settings +// ----------------------------------------------------------------------------- + + module.exports = Graph3d; diff --git a/lib/graph3d/Settings.js b/lib/graph3d/Settings.js new file mode 100644 index 00000000..7a702580 --- /dev/null +++ b/lib/graph3d/Settings.js @@ -0,0 +1,436 @@ +//////////////////////////////////////////////////////////////////////////////// +// This modules handles the options for Graph3d. +// +//////////////////////////////////////////////////////////////////////////////// +var Camera = require('./Camera'); +var Point3d = require('./Point3d'); + + +// enumerate the available styles +var STYLE = { + BAR : 0, + BARCOLOR: 1, + BARSIZE : 2, + DOT : 3, + DOTLINE : 4, + DOTCOLOR: 5, + DOTSIZE : 6, + GRID : 7, + LINE : 8, + SURFACE : 9 +}; + + +// The string representations of the styles +var STYLENAME = { + 'dot' : STYLE.DOT, + 'dot-line' : STYLE.DOTLINE, + 'dot-color': STYLE.DOTCOLOR, + 'dot-size' : STYLE.DOTSIZE, + 'line' : STYLE.LINE, + 'grid' : STYLE.GRID, + 'surface' : STYLE.SURFACE, + 'bar' : STYLE.BAR, + 'bar-color': STYLE.BARCOLOR, + 'bar-size' : STYLE.BARSIZE +}; + + +/** + * 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', + 'dotSizeRatio', + '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' +]; + + +// Placeholder for DEFAULTS reference +var DEFAULTS = undefined; + + +/** + * Check if given hash is empty. + * + * Source: http://stackoverflow.com/a/679937 + */ +function isEmpty(obj) { + for(var prop in obj) { + if (obj.hasOwnProperty(prop)) + return false; + } + + return true; +} + + + +/** + * 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]; + } +} + + +/** + * Initialize dst with the values in src. + * + * src is the hash with the default values. + * A reference DEFAULTS to this hash is stored locally for + * further handling. + * + * For now, dst is assumed to be a Graph3d instance. + */ +function setDefaults(src, dst) { + if (src === undefined || isEmpty(src)) { + throw new Error('No DEFAULTS passed'); + } + if (dst === undefined) { + throw new Error('No dst passed'); + } + + // Remember defaults for future reference + DEFAULTS = src; + + // Handle the defaults which can be simply copied over + forceCopy(src, dst, OPTIONKEYS); + forceCopy(src, dst, PREFIXEDOPTIONKEYS, 'default'); + + // Handle the more complex ('special') fields + setSpecialSettings(src, dst); + + // Following are internal fields, not part of the user settings + dst.margin = 10; // px + dst.showGrayBottom = false; // TODO: this does not work correctly + dst.showTooltip = false; + dst.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window? +} + + +function setOptions(options, dst) { + if (options === undefined) { + return; + } + if (dst === undefined) { + throw new Error('No dst passed'); + } + + if (DEFAULTS === undefined || isEmpty(DEFAULTS)) { + throw new Error('DEFAULTS not set for module Settings'); + } + + + // Handle the parameters which can be simply copied over + safeCopy(options, dst, OPTIONKEYS); + safeCopy(options, dst, PREFIXEDOPTIONKEYS, 'default'); + + // Handle the more complex ('special') fields + setSpecialSettings(options, dst); +} + + +/** + * Special handling for certain parameters + * + * 'Special' here means: setting requires more than a simple copy + */ +function setSpecialSettings(src, dst) { + if (src.backgroundColor !== undefined) { + setBackgroundColor(src.backgroundColor, dst); + } + + setDataColor(src.dataColor, dst); + setStyle(src.style, dst); + setShowLegend(src.showLegend, dst); + 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. + */ +function setShowLegend(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 = dst.style === STYLE.DOTCOLOR + || dst.style === STYLE.DOTSIZE; + + dst.showLegend = isLegendGraphStyle; + } else { + // Leave current value as is + } + } else { + dst.showLegend = showLegend; + } +} + + +/** + * Retrieve the style index from given styleName + * @param {string} styleName Style name such as 'dot', 'grid', 'dot-line' + * @return {Number} styleNumber Enumeration value representing the style, or -1 + * when not found + */ +function getStyleNumberByName(styleName) { + var number = STYLENAME[styleName]; + + if (number === undefined) { + return -1; + } + + return number; +} + + +/** + * Check if given number is a valid style number. + * + * @return true if valid, false otherwise + */ +function checkStyleNumber(style) { + var valid = false; + + for (var n in STYLE) { + if (STYLE[n] === style) { + valid = true; + break; + } + } + + return valid; +} + + +function setStyle(style, dst) { + if (style === undefined) { + return; // Nothing to do + } + + var styleNumber; + + if (typeof style === 'string') { + styleNumber = getStyleNumberByName(style); + + if (styleNumber === -1 ) { + throw new Error('Style \'' + style + '\' is invalid'); + } + } else { + // Do a pedantic check on style number value + if (!checkStyleNumber(style)) { + 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 + */ +function setBackgroundColor(backgroundColor, dst) { + var fill = 'white'; + var stroke = 'gray'; + var strokeWidth = 1; + + if (typeof(backgroundColor) === 'string') { + fill = backgroundColor; + stroke = 'none'; + strokeWidth = 0; + } + else if (typeof(backgroundColor) === 'object') { + if (backgroundColor.fill !== undefined) fill = backgroundColor.fill; + if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke; + if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth; + } + 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'; +} + + +function setDataColor(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 { + 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; + } + } +} + + +function setCameraPosition(cameraPosition, dst) { + var camPos = cameraPosition; + if (camPos === undefined) { + return; + } + + if (dst.camera === undefined) { + dst.camera = new Camera(); + } + + dst.camera.setArmRotation(camPos.horizontal, camPos.vertical); + dst.camera.setArmLength(camPos.distance); +} + + +module.exports.STYLE = STYLE; +module.exports.setDefaults = setDefaults; +module.exports.setOptions = setOptions; +module.exports.setCameraPosition = setCameraPosition; diff --git a/lib/timeline/Core.js b/lib/timeline/Core.js index 612a6fc1..04c51ad6 100644 --- a/lib/timeline/Core.js +++ b/lib/timeline/Core.js @@ -82,7 +82,6 @@ Core.prototype._create = function (container) { this.dom.centerContainer.appendChild(this.dom.center); this.dom.leftContainer.appendChild(this.dom.left); this.dom.rightContainer.appendChild(this.dom.right); - this.dom.centerContainer.appendChild(this.dom.shadowTop); this.dom.centerContainer.appendChild(this.dom.shadowBottom); this.dom.leftContainer.appendChild(this.dom.shadowTopLeft); @@ -90,13 +89,30 @@ Core.prototype._create = function (container) { this.dom.rightContainer.appendChild(this.dom.shadowTopRight); this.dom.rightContainer.appendChild(this.dom.shadowBottomRight); + // size properties of each of the panels + this.props = { + root: {}, + background: {}, + centerContainer: {}, + leftContainer: {}, + rightContainer: {}, + center: {}, + left: {}, + right: {}, + top: {}, + bottom: {}, + border: {}, + scrollTop: 0, + scrollTopMin: 0 + }; + this.on('rangechange', function () { if (this.initialDrawDone === true) { - this._redraw(); // this allows overriding the _redraw method + this._redraw(); } }.bind(this)); this.on('touch', this._onTouch.bind(this)); - this.on('pan', this._onDrag.bind(this)); + this.on('panmove', this._onDrag.bind(this)); var me = this; this.on('_change', function (properties) { @@ -154,16 +170,15 @@ Core.prototype._create = function (container) { }.bind(this)); function onMouseWheel(event) { - if (me.isActive()) { - me.emit('mousewheel', event); + if (this.isActive()) { + this.emit('mousewheel', event); } - } - this.dom.root.addEventListener('mousewheel', onMouseWheel); - this.dom.root.addEventListener('DOMMouseScroll', onMouseWheel); - function onMouseWheelCenter(event) { // prevent scrolling when zoomKey defined or activated - if (!me.options.zoomKey || event[me.options.zoomKey]) return + if (!this.options.zoomKey || event[this.options.zoomKey]) return + + // prevent scrolling vertically when horizontalScroll is true + if (this.options.horizontalScroll) return var delta = 0; if (event.wheelDelta) { /* IE/Opera. */ @@ -174,12 +189,17 @@ Core.prototype._create = function (container) { delta = -event.detail / 3; } - var current = me.props.scrollTop; + var current = this.props.scrollTop; var adjusted = current + delta * 120; - if (me.isActive()) { - me._setScrollTop(adjusted); - me._redraw(); - me.emit('scroll', event); + + if (this.isActive()) { + this._setScrollTop(adjusted); + if (this.options.verticalScroll) { + this.dom.left.parentNode.scrollTop = -adjusted; + this.dom.right.parentNode.scrollTop = -adjusted; + } + this._redraw(); + this.emit('scroll', event); } // Prevent default actions caused by mouse wheel @@ -187,25 +207,29 @@ Core.prototype._create = function (container) { event.preventDefault(); } - this.dom.center.addEventListener('mousewheel', onMouseWheelCenter); - this.dom.center.addEventListener('DOMMouseScroll', onMouseWheelCenter); + if (this.dom.center.addEventListener) { + // IE9, Chrome, Safari, Opera + this.dom.center.addEventListener("mousewheel", onMouseWheel.bind(this), false); + // Firefox + this.dom.center.addEventListener("DOMMouseScroll", onMouseWheel.bind(this), false); + } else { + // IE 6/7/8 + this.dom.center.attachEvent("onmousewheel", onMouseWheel.bind(this)); + } - // size properties of each of the panels - this.props = { - root: {}, - background: {}, - centerContainer: {}, - leftContainer: {}, - rightContainer: {}, - center: {}, - left: {}, - right: {}, - top: {}, - bottom: {}, - border: {}, - scrollTop: 0, - scrollTopMin: 0 - }; + function onMouseScrollSide(event) { + if (!me.options.verticalScroll) return; + event.preventDefault(); + if (me.isActive()) { + var adjusted = -event.target.scrollTop; + me._setScrollTop(adjusted); + me._redraw(); + me.emit('scrollSide', event); + } + } + + this.dom.left.parentNode.addEventListener('scroll', onMouseScrollSide.bind(this)); + this.dom.right.parentNode.addEventListener('scroll', onMouseScrollSide.bind(this)); this.customTimes = []; @@ -251,17 +275,23 @@ Core.prototype.setOptions = function (options) { var fields = [ 'width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'clickToUse', 'dataAttributes', 'hiddenDates', - 'locale', 'locales', 'moment', 'rtl', 'zoomKey' + 'locale', 'locales', 'moment', 'rtl', 'zoomKey', 'horizontalScroll', 'verticalScroll' ]; util.selectiveExtend(fields, this.options, options); if (this.options.rtl) { - var contentContainer = this.dom.leftContainer; - 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'; + } + + if (this.options.verticalScroll) { + if (this.options.rtl) { + this.dom.rightContainer.className = 'vis-panel vis-right vis-vertical-scroll'; + } else { + this.dom.leftContainer.className = 'vis-panel vis-left vis-vertical-scroll'; + } + } this.options.orientation = {item:undefined,axis:undefined}; if ('orientation' in options) { @@ -734,9 +764,25 @@ Core.prototype._redraw = function() { // calculate the widths of the panels props.root.width = dom.root.offsetWidth; props.background.width = props.root.width - borderRootWidth; - props.left.width = dom.leftContainer.clientWidth || -props.border.left; + + if (!this.initialDrawDone) { + props.scrollbarWidth = util.getScrollBarWidth(); + } + + if (this.options.verticalScroll) { + if (this.options.rtl) { + props.left.width = dom.leftContainer.clientWidth || -props.border.left; + props.right.width = dom.rightContainer.clientWidth + props.scrollbarWidth || -props.border.right; + } else { + props.left.width = dom.leftContainer.clientWidth + props.scrollbarWidth || -props.border.left; + props.right.width = dom.rightContainer.clientWidth || -props.border.right; + } + } else { + props.left.width = dom.leftContainer.clientWidth || -props.border.left; + props.right.width = dom.rightContainer.clientWidth || -props.border.right; + } + props.leftContainer.width = props.left.width; - props.right.width = dom.rightContainer.clientWidth || -props.border.right; props.rightContainer.width = props.right.width; var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth; props.center.width = centerWidth; @@ -779,10 +825,9 @@ Core.prototype._redraw = function() { // update the scrollTop, feasible range for the offset can be changed // when the height of the Core or of the contents of the center changed - this._updateScrollTop(); + var offset = this._updateScrollTop(); // reposition the scrollable contents - var offset = this.props.scrollTop; if (options.orientation.item != 'top') { offset += Math.max(this.props.centerContainer.height - this.props.center.height - this.props.border.top - this.props.border.bottom, 0); @@ -790,10 +835,8 @@ Core.prototype._redraw = function() { dom.center.style.left = '0'; dom.center.style.top = offset + 'px'; dom.left.style.left = '0'; - dom.left.style.top = offset + 'px'; dom.right.style.left = '0'; - dom.right.style.top = offset + 'px'; - + // show shadows when vertical scrolling is available var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : ''; var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : ''; @@ -804,6 +847,16 @@ Core.prototype._redraw = function() { dom.shadowTopRight.style.visibility = visibilityTop; dom.shadowBottomRight.style.visibility = visibilityBottom; + if (this.options.verticalScroll) { + dom.shadowTopRight.style.visibility = "hidden"; + dom.shadowBottomRight.style.visibility = "hidden"; + dom.shadowTopLeft.style.visibility = "hidden"; + dom.shadowBottomLeft.style.visibility = "hidden"; + } else { + dom.left.style.top = offset + 'px'; + dom.right.style.top = offset + 'px'; + } + // enable/disable vertical panning var contentsOverflow = this.props.center.height > this.props.centerContainer.height; this.hammer.get('pan').set({ @@ -826,6 +879,7 @@ Core.prototype._redraw = function() { } else { this.redrawCount = 0; } + this.initialDrawDone = true; //Emit public 'changed' event for UI updates, see issue #1592 @@ -1018,6 +1072,7 @@ Core.prototype._onPinch = function (event) { * @private */ Core.prototype._onDrag = function (event) { + if (!event) return // refuse to drag when we where pinching to prevent the timeline make a jump // when releasing the fingers in opposite order from the touch screen if (!this.touch.allowDragging) return; @@ -1027,6 +1082,10 @@ Core.prototype._onDrag = function (event) { var oldScrollTop = this._getScrollTop(); var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta); + if (this.options.verticalScroll) { + this.dom.left.parentNode.scrollTop = -this.props.scrollTop; + this.dom.right.parentNode.scrollTop = -this.props.scrollTop; + } if (newScrollTop != oldScrollTop) { this.emit("verticalDrag"); diff --git a/lib/timeline/Range.js b/lib/timeline/Range.js index 41e500af..faa07abe 100644 --- a/lib/timeline/Range.js +++ b/lib/timeline/Range.js @@ -81,7 +81,7 @@ Range.prototype.setOptions = function (options) { // copy the options that we know var fields = [ 'direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable', - 'moment', 'activate', 'hiddenDates', 'zoomKey', 'rtl' + 'moment', 'activate', 'hiddenDates', 'zoomKey', 'rtl', 'horizontalScroll' ]; util.selectiveExtend(fields, this.options, options); @@ -390,6 +390,8 @@ Range.prototype._onDragStart = function(event) { * @private */ Range.prototype._onDrag = function (event) { + if (!event) return + if (!this.props.touch.dragging) return; // only allow dragging when configured as movable @@ -445,6 +447,9 @@ Range.prototype._onDrag = function (event) { end: endDate, byUser: true }); + + // fire a panmove event + this.body.emitter.emit('panmove'); }; /** @@ -483,15 +488,10 @@ Range.prototype._onDragEnd = function (event) { * @private */ Range.prototype._onMouseWheel = function(event) { - // only allow zooming when configured as zoomable and moveable - if (!(this.options.zoomable && this.options.moveable)) return; - - // only zoom when the mouse is inside the current range - if (!this._isInsideRange(event)) return; + // Prevent default actions caused by mouse wheel + // (else the page and timeline both zoom and scroll) + event.preventDefault(); - // only zoom when the according key is pressed and the zoomKey option is set - if (this.options.zoomKey && !event[this.options.zoomKey]) return; - // retrieve delta var delta = 0; if (event.wheelDelta) { /* IE/Opera. */ @@ -502,6 +502,27 @@ Range.prototype._onMouseWheel = function(event) { delta = -event.detail / 3; } + // don't allow zoom when the according key is pressed and the zoomKey option or not zoomable but movable + if ((this.options.zoomKey && !event[this.options.zoomKey] && this.options.zoomable) + || (!this.options.zoomable && this.options.moveable)) { + if (this.options.horizontalScroll) { + // calculate a single scroll jump relative to the range scale + var diff = delta * (this.end - this.start) / 20; + // calculate new start and end + var newStart = this.start - diff; + var newEnd = this.end - diff; + + this.setRange(newStart, newEnd); + } + return; + } + + // only allow zooming when configured as zoomable and moveable + if (!(this.options.zoomable && this.options.moveable)) return; + + // only zoom when the mouse is inside the current range + if (!this._isInsideRange(event)) return; + // If delta is nonzero, handle it. // Basically, delta is now positive if wheel was scrolled up, // and negative, if wheel was scrolled down. @@ -524,10 +545,6 @@ Range.prototype._onMouseWheel = function(event) { this.zoom(scale, pointerDate, delta); } - - // Prevent default actions caused by mouse wheel - // (else the page and timeline both zoom and scroll) - event.preventDefault(); }; /** diff --git a/lib/timeline/Timeline.js b/lib/timeline/Timeline.js index a8e55e92..05da12ac 100644 --- a/lib/timeline/Timeline.js +++ b/lib/timeline/Timeline.js @@ -28,8 +28,8 @@ import Validator from '../shared/Validator'; * @constructor * @extends Core */ -function Timeline (container, items, groups, options) { +function Timeline (container, items, groups, options) { if (!(this instanceof Timeline)) { throw new SyntaxError('Constructor must be called with the new operator'); } @@ -52,7 +52,6 @@ function Timeline (container, items, groups, options) { axis: 'bottom', // axis orientation: 'bottom', 'top', or 'both' item: 'bottom' // not relevant }, - rtl: false, moment: moment, width: null, @@ -105,6 +104,11 @@ function Timeline (container, items, groups, options) { // current time bar this.currentTime = new CurrentTime(this.body); this.components.push(this.currentTime); + + // apply options + if (options) { + this.setOptions(options); + } // item set this.itemSet = new ItemSet(this.body, this.options); @@ -145,11 +149,6 @@ function Timeline (container, items, groups, options) { } }); - // apply options - if (options) { - this.setOptions(options); - } - // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS! if (groups) { this.setGroups(groups); @@ -546,25 +545,4 @@ Timeline.prototype.getEventProperties = function (event) { } }; - /** - * Extend the drag event handler from Core, move the timeline vertically - * @param {Event} event - * @private - */ - Timeline.prototype._onDrag = function (event) { - // refuse to drag when we where pinching to prevent the timeline make a jump - // when releasing the fingers in opposite order from the touch screen, and refuse - // to drag when an item is already being dragged - if (!this.touch.allowDragging || this.itemSet.touchParams.itemIsDragging) return; - - var delta = event.deltaY; - - var oldScrollTop = this._getScrollTop(); - var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta); - - if (newScrollTop != oldScrollTop) { - this.emit("verticalDrag"); - } - }; - module.exports = Timeline; diff --git a/lib/timeline/component/Group.js b/lib/timeline/component/Group.js index f522fd82..691d24b1 100644 --- a/lib/timeline/component/Group.js +++ b/lib/timeline/component/Group.js @@ -87,10 +87,12 @@ Group.prototype._create = function() { Group.prototype.setData = function(data) { // update contents var content; + var templateFunction; + if (this.itemSet.options && this.itemSet.options.groupTemplate) { - content = this.itemSet.options.groupTemplate(data, this.dom.inner); - } - else { + templateFunction = this.itemSet.options.groupTemplate.bind(this); + content = templateFunction(data, this.dom.inner); + } else { content = data && data.content; } @@ -100,11 +102,11 @@ Group.prototype.setData = function(data) { this.dom.inner.removeChild(this.dom.inner.firstChild); } this.dom.inner.appendChild(content); - } - else if (content !== undefined && content !== null) { + } else if (content instanceof Object) { + templateFunction(data, this.dom.inner); + } else if (content !== undefined && content !== null) { this.dom.inner.innerHTML = content; - } - else { + } else { this.dom.inner.innerHTML = this.groupId || ''; // groupId can be null } diff --git a/lib/timeline/component/ItemSet.js b/lib/timeline/component/ItemSet.js index 77912821..9eb46393 100644 --- a/lib/timeline/component/ItemSet.js +++ b/lib/timeline/component/ItemSet.js @@ -27,7 +27,6 @@ var BACKGROUND = '__background__'; // reserved group id for background items wit function ItemSet(body, options) { this.body = body; this.defaultOptions = { - rtl: false, type: null, // 'box', 'point', 'range', 'background' orientation: { item: 'bottom' // item orientation: 'top' or 'bottom' @@ -96,7 +95,8 @@ function ItemSet(body, options) { // options is shared by this ItemSet and all its items this.options = util.extend({}, this.defaultOptions); - + this.options.rtl = options.rtl; + // options for getting items from the DataSet with the correct type this.itemOptions = { type: {start: 'Date', end: 'Date'} @@ -230,7 +230,12 @@ ItemSet.prototype._create = function(){ // add item on doubletap this.hammer.on('doubletap', this._onAddItem.bind(this)); - this.groupHammer = new Hammer(this.body.dom.leftContainer); + + if (this.options.rtl) { + this.groupHammer = new Hammer(this.body.dom.rightContainer); + } else { + this.groupHammer = new Hammer(this.body.dom.leftContainer); + } this.groupHammer.on('panstart', this._onGroupDragStart.bind(this)); this.groupHammer.on('panmove', this._onGroupDrag.bind(this)); @@ -451,7 +456,11 @@ ItemSet.prototype.show = function() { // show labelset containing labels if (!this.dom.labelSet.parentNode) { - this.body.dom.left.appendChild(this.dom.labelSet); + if (this.options.rtl) { + this.body.dom.right.appendChild(this.dom.labelSet); + } else { + this.body.dom.left.appendChild(this.dom.labelSet); + } } }; diff --git a/lib/timeline/component/css/item.css b/lib/timeline/component/css/item.css index 41d22256..0e9327ae 100644 --- a/lib/timeline/component/css/item.css +++ b/lib/timeline/component/css/item.css @@ -136,6 +136,15 @@ color: white; } +.vis-item .vis-drag-center { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0px; + cursor: move; +} + .vis-item.vis-range .vis-drag-left { position: absolute; width: 24px; diff --git a/lib/timeline/component/css/panel.css b/lib/timeline/component/css/panel.css index 4c5088a9..a02ee5bc 100644 --- a/lib/timeline/component/css/panel.css +++ b/lib/timeline/component/css/panel.css @@ -1,4 +1,3 @@ - .vis-panel { position: absolute; @@ -24,6 +23,28 @@ overflow: hidden; } +.vis-left.vis-panel.vis-vertical-scroll, .vis-right.vis-panel.vis-vertical-scroll { + height: 100%; + overflow-x: hidden; + overflow-y: scroll; +} + +.vis-left.vis-panel.vis-vertical-scroll { + direction: rtl; +} + +.vis-left.vis-panel.vis-vertical-scroll .vis-content { + direction: ltr; +} + +.vis-right.vis-panel.vis-vertical-scroll { + direction: ltr; +} + +.vis-right.vis-panel.vis-vertical-scroll .vis-content { + direction: rtl; +} + .vis-panel.vis-center, .vis-panel.vis-top, .vis-panel.vis-bottom { diff --git a/lib/timeline/component/item/BoxItem.js b/lib/timeline/component/item/BoxItem.js index f77652bf..2963619b 100644 --- a/lib/timeline/component/item/BoxItem.js +++ b/lib/timeline/component/item/BoxItem.js @@ -164,6 +164,7 @@ BoxItem.prototype.redraw = function() { this.dirty = false; } + this._repaintDragCenter(); this._repaintDeleteButton(dom.box); }; diff --git a/lib/timeline/component/item/Item.js b/lib/timeline/component/item/Item.js index e50918a8..c12a3203 100644 --- a/lib/timeline/component/item/Item.js +++ b/lib/timeline/component/item/Item.js @@ -187,31 +187,37 @@ Item.prototype._repaintDeleteButton = function (anchor) { */ Item.prototype._updateContents = function (element) { var content; + var templateFunction; + 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, element); - } - else { + templateFunction = this.options.template.bind(this); + content = templateFunction(itemData, element); + } else { content = this.data.content; } - var changed = this._contentToString(this.content) !== this._contentToString(content); - if (changed) { - // only replace the content when changed - if (content instanceof Element) { - element.innerHTML = ''; - element.appendChild(content); - } - else if (content != undefined) { - element.innerHTML = content; - } - else { - if (!(this.data.type == 'background' && this.data.content === undefined)) { - throw new Error('Property "content" missing in item ' + this.id); + if (content instanceof Object) { + templateFunction(itemData, element) + } else { + var changed = this._contentToString(this.content) !== this._contentToString(content); + if (changed) { + // only replace the content when changed + if (content instanceof Element) { + element.innerHTML = ''; + element.appendChild(content); + } + else if (content != undefined) { + element.innerHTML = content; + } + else { + if (!(this.data.type == 'background' && this.data.content === undefined)) { + throw new Error('Property "content" missing in item ' + this.id); + } } - } - this.content = content; + this.content = content; + } } }; @@ -281,6 +287,7 @@ Item.prototype._updateStyle = function(element) { } }; + /** * Stringify the items contents * @param {string | Element | undefined} content @@ -309,4 +316,27 @@ Item.prototype.getWidthRight = function () { return 0; }; +/** + * Repaint a drag area on the center of the item when the item is selected + * @protected + */ +Item.prototype._repaintDragCenter = function () { + if (this.selected && this.options.editable.updateTime && !this.dom.dragCenter) { + // create and show drag area + var dragCenter = document.createElement('div'); + dragCenter.className = 'vis-drag-center'; + dragCenter.dragCenterItem = this; + + this.dom.box.appendChild(dragCenter); + this.dom.dragCenter = dragCenter; + } + else if (!this.selected && this.dom.dragCenter) { + // delete drag area + if (this.dom.dragCenter.parentNode) { + this.dom.dragCenter.parentNode.removeChild(this.dom.dragCenter); + } + this.dom.dragCenter = null; + } +}; + module.exports = Item; diff --git a/lib/timeline/component/item/PointItem.js b/lib/timeline/component/item/PointItem.js index 10a2463c..91b86909 100644 --- a/lib/timeline/component/item/PointItem.js +++ b/lib/timeline/component/item/PointItem.js @@ -140,7 +140,8 @@ PointItem.prototype.redraw = function() { this.dirty = false; } - + + this._repaintDragCenter(); this._repaintDeleteButton(dom.point); }; diff --git a/lib/timeline/component/item/RangeItem.js b/lib/timeline/component/item/RangeItem.js index 8ec29991..5e287f4f 100644 --- a/lib/timeline/component/item/RangeItem.js +++ b/lib/timeline/component/item/RangeItem.js @@ -124,6 +124,7 @@ RangeItem.prototype.redraw = function() { this.dirty = false; } this._repaintDeleteButton(dom.box); + this._repaintDragCenter(); this._repaintDragLeft(); this._repaintDragRight(); }; diff --git a/lib/timeline/optionsTimeline.js b/lib/timeline/optionsTimeline.js index a20d27f4..4c976d85 100644 --- a/lib/timeline/optionsTimeline.js +++ b/lib/timeline/optionsTimeline.js @@ -26,6 +26,8 @@ let allOptions = { //globals : align: {string}, rtl: {boolean, 'undefined': 'undefined'}, + verticalScroll: {boolean, 'undefined': 'undefined'}, + horizontalScroll: {boolean, 'undefined': 'undefined'}, autoResize: {boolean}, clickToUse: {boolean}, dataAttributes: {string, array}, diff --git a/lib/util.js b/lib/util.js index c1dc1c21..f9506a6a 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1452,3 +1452,29 @@ exports.easingFunctions = { return t < .5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t } }; + +exports.getScrollBarWidth = function () { + var inner = document.createElement('p'); + inner.style.width = "100%"; + inner.style.height = "200px"; + + var outer = document.createElement('div'); + outer.style.position = "absolute"; + outer.style.top = "0px"; + outer.style.left = "0px"; + outer.style.visibility = "hidden"; + outer.style.width = "200px"; + outer.style.height = "150px"; + outer.style.overflow = "hidden"; + outer.appendChild (inner); + + document.body.appendChild (outer); + var w1 = inner.offsetWidth; + outer.style.overflow = 'scroll'; + var w2 = inner.offsetWidth; + if (w1 == w2) w2 = outer.clientWidth; + + document.body.removeChild (outer); + + return (w1 - w2); +}; \ No newline at end of file diff --git a/misc/how_to_help.md b/misc/how_to_help.md index fe16e1e6..63514fa4 100644 --- a/misc/how_to_help.md +++ b/misc/how_to_help.md @@ -6,13 +6,13 @@ The company that developed vis.js for the main part, *almende* is [not able to m ### 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! +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. +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 @@ -28,14 +28,14 @@ If you use vis.js to develop something beautiful feel free to create a pull-requ ### 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) ;-) +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. +A lot of people have a lot of ideas for improving vis.js. [We label these issues as **Feature-Request**](https://github.com/almende/vis/labels/Feature-Request). 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. @@ -64,4 +64,6 @@ There are some rules for pull-request: * Always adapt to the code style of the existing source. Never adapt existing code to your personal taste. :trollface: +* Pull-requests must be reviewed by at least two member of the support team. The First must approve the pull-request, the second can than merge after also checking it. + **Happy Helping!!**