From 28a4d44ae6ade4317828d3febb9bd290f885e17f Mon Sep 17 00:00:00 2001 From: Ivo Silva Date: Sat, 22 Apr 2017 13:35:49 +0200 Subject: [PATCH 01/28] Fix #2614: Timeline docs border overlaps (#2992) --- docs/css/style.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/css/style.css b/docs/css/style.css index f7048a9c..3a1d060e 100644 --- a/docs/css/style.css +++ b/docs/css/style.css @@ -106,6 +106,8 @@ table.events td:nth-child(2) { pre { margin: 20px 0; + white-space: pre-wrap; + max-width: 100%; } a code { From 0ae676c424e6a0b2dc72b1b5bbb04da5c9520464 Mon Sep 17 00:00:00 2001 From: Ben Morton Date: Wed, 26 Apr 2017 18:25:33 +0100 Subject: [PATCH 02/28] Add check for empty groupIds array and get full list from data set. Fixes #2877 (#2986) --- lib/timeline/component/ItemSet.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/timeline/component/ItemSet.js b/lib/timeline/component/ItemSet.js index fe2b46f6..e0019df8 100644 --- a/lib/timeline/component/ItemSet.js +++ b/lib/timeline/component/ItemSet.js @@ -2233,8 +2233,16 @@ ItemSet.prototype.itemFromRelatedTarget = function(event) { */ ItemSet.prototype.groupFromTarget = function(event) { var clientY = event.center ? event.center.y : event.clientY; - for (var i = 0; i < this.groupIds.length; i++) { - var groupId = this.groupIds[i]; + var groupIds = this.groupIds; + + if (groupIds.length <= 0) { + groupIds = this.groupsData.getIds({ + order: this.options.groupOrder + }); + } + + for (var i = 0; i < groupIds.length; i++) { + var groupId = groupIds[i]; var group = this.groups[groupId]; var foreground = group.dom.foreground; var top = util.getAbsoluteTop(foreground); From 253e52a9483945e901f9267989faa44006d1b2a0 Mon Sep 17 00:00:00 2001 From: wimrijnders Date: Fri, 28 Apr 2017 07:02:08 +0200 Subject: [PATCH 03/28] Fix #2994: select edge with id zero (#2996) --- lib/network/modules/SelectionHandler.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/network/modules/SelectionHandler.js b/lib/network/modules/SelectionHandler.js index 121a2e93..101b9f4a 100644 --- a/lib/network/modules/SelectionHandler.js +++ b/lib/network/modules/SelectionHandler.js @@ -156,7 +156,7 @@ class SelectionHandler { /** - * Get the top node at the a specific point (like a click) + * Get the top node at the passed point (like a click) * * @param {{x: Number, y: Number}} pointer * @return {Node | undefined} node @@ -212,11 +212,10 @@ class SelectionHandler { /** - * Place holder. To implement change the getNodeAt to a _getObjectAt. Have the _getObjectAt call - * getNodeAt and _getEdgesAt, then priortize the selection to user preferences. + * Get the edges nearest to the passed point (like a click) * - * @param pointer - * @returns {undefined} + * @param {{x: Number, y: Number}} pointer + * @return {Edge | undefined} node */ getEdgeAt(pointer, returnEdge = true) { // Iterate over edges, pick closest within 10 @@ -239,7 +238,7 @@ class SelectionHandler { } } } - if (overlappingEdge) { + if (overlappingEdge !== null) { if (returnEdge === true) { return this.body.edges[overlappingEdge]; } @@ -706,4 +705,4 @@ class SelectionHandler { } } -export default SelectionHandler; \ No newline at end of file +export default SelectionHandler; From 79bb381c21ff7844feb9faebb73497052ceb7132 Mon Sep 17 00:00:00 2001 From: Ben Morton Date: Fri, 28 Apr 2017 06:46:43 +0100 Subject: [PATCH 04/28] Add check for parent existence when changing group in Item.setData. (#2985) Add subgroup updates to Item.setData. Remove group updating logic from ItemSet._updateItem. Fixes #2939 --- lib/timeline/component/ItemSet.js | 21 +++------------------ lib/timeline/component/item/Item.js | 7 ++++++- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/lib/timeline/component/ItemSet.js b/lib/timeline/component/ItemSet.js index e0019df8..002f294a 100644 --- a/lib/timeline/component/ItemSet.js +++ b/lib/timeline/component/ItemSet.js @@ -1196,14 +1196,6 @@ ItemSet.prototype._addItem = function(item) { * @private */ ItemSet.prototype._updateItem = function(item, itemData) { - var oldGroupId = item.data.group; - var oldSubGroupId = item.data.subgroup; - - if (oldGroupId != itemData.group) { - var oldGroup = this.groups[oldGroupId]; - if (oldGroup) oldGroup.remove(item); - } - // update the items data (will redraw the item when displayed) item.setData(itemData); @@ -1214,14 +1206,6 @@ ItemSet.prototype._updateItem = function(item, itemData) { } else if (group && group.data && group.data.showNested) { item.groupShowing = true; } - // update group - if (group) { - if (oldGroupId != item.data.group) { - group.add(item); - } else if (oldSubGroupId != item.data.subgroup) { - group.changeSubgroup(item, oldSubGroupId); - } - } }; /** @@ -1587,10 +1571,11 @@ ItemSet.prototype._moveToGroup = function(item, groupId) { var oldGroup = item.parent; oldGroup.remove(item); oldGroup.order(); + + item.data.group = group.groupId; + group.add(item); group.order(); - - item.data.group = group.groupId; } }; diff --git a/lib/timeline/component/item/Item.js b/lib/timeline/component/item/Item.js index 6713a7ac..d7269bae 100644 --- a/lib/timeline/component/item/Item.js +++ b/lib/timeline/component/item/Item.js @@ -61,9 +61,14 @@ Item.prototype.unselect = function() { */ Item.prototype.setData = function(data) { var groupChanged = data.group != undefined && this.data.group != data.group; - if (groupChanged) { + if (groupChanged && this.parent != null) { this.parent.itemSet._moveToGroup(this, data.group); } + + var subGroupChanged = data.subgroup != undefined && this.data.subgroup != data.subgroup; + if (subGroupChanged && this.parent != null) { + this.parent.changeSubgroup(this, this.data.subgroup, data.subgroup); + } this.data = data; this._updateEditStatus(); From e17136a88d7e06f9bb032d45a01484f533ab9d9b Mon Sep 17 00:00:00 2001 From: wimrijnders Date: Sun, 30 Apr 2017 15:42:02 +0200 Subject: [PATCH 05/28] Fix placement label for dot shape (#3018) --- lib/network/modules/components/nodes/util/ShapeBase.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/network/modules/components/nodes/util/ShapeBase.js b/lib/network/modules/components/nodes/util/ShapeBase.js index c11dd4c8..646e36b5 100644 --- a/lib/network/modules/components/nodes/util/ShapeBase.js +++ b/lib/network/modules/components/nodes/util/ShapeBase.js @@ -47,7 +47,9 @@ class ShapeBase extends NodeBase { ctx.restore(); if (this.options.label !== undefined) { - let yLabel = y + 0.5 * this.height + 3; // the + 3 is to offset it a bit below the node. + // Need to call following here in order to ensure value for `this.labelModule.size.height` + this.labelModule.calculateLabelSize(ctx, selected, hover, x, y, 'hanging') + let yLabel = y + 0.5 * this.height + 0.5 * this.labelModule.size.height; this.labelModule.draw(ctx, x, yLabel, selected, hover, 'hanging'); } @@ -63,7 +65,7 @@ class ShapeBase extends NodeBase { if (this.options.label !== undefined && this.labelModule.size.width > 0) { this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); - this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height + 3); + this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height); } } From 6bffd4921a27cc89940dde9b2b56e878dfa8ecb6 Mon Sep 17 00:00:00 2001 From: Alexander Wunschik Date: Sun, 30 Apr 2017 16:25:20 +0200 Subject: [PATCH 06/28] added documentation on how labels are used (#2873) * added documentation on how labels are used * added documentation on how labels are used * applied change requests * change requests from @wimrijnders * added documentation on how labels are used * applied change requests * change requests from @wimrijnders * changes requested by @bradh --- misc/labels.md | 106 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 misc/labels.md diff --git a/misc/labels.md b/misc/labels.md new file mode 100644 index 00000000..8307c374 --- /dev/null +++ b/misc/labels.md @@ -0,0 +1,106 @@ +# How we use Github labels + +*Because only team members can add and change labels this document is mainly for maintainers, but also for users to understand how we use labels.* + +*It is important to also label old and closed issues uniformly in order to export them later e.g. if the project gets separated into multiple components.* + + +## Issue Types +If an issue was created it MUST always be labeled as one of the following issue types: + +### `Question` +The author has a general or very specific question.
+If it is a general question on how to use vis.js the issues should be closed immediately with a reference to [stackoverflow](https://stackoverflow.com/questions/tagged/vis.js).
+Specific question or hard problems should stay open.
+Questions should be closed within 3 months. + +### `Problem` +This issues points to a potential bug that needs to be confirmed.
+If the problem most likely originates from the user's code it should be labeled as [`Question`](#question) instead.
+The support team should try to reproduce this issue and then close it or mark it as [`Confirmed Bug`](#confirmed-bug). + +### `Confirmed Bug` +This issue was reported as [`Problem`](#problem), but the issue is reproducible and is now a confirmed bug. + +### `Feature-Request` +This issue proposes a new feature or a change of existing functionality. Issues that are unlikely to get implemented should be closed. + +### `wontfix` +This issues is e.g. for discussing a topic or for project management purposes, and is not handled in the usual issue process. + + +## Graph type +All issues MUST have one of the following type labels. These labels are usually mutually exclusive: + +### `DataSet` +Concerns the DataSet implementation. + +### `Graph2D` +Concerns the 2D-Graph implementation. + +### `Graph3D` +Concerns the 3D-Graph implementation. + +### `Network` +Concerns the Network-Graph implementation. + +### `source/non-public API` +This issues is just for discussion or is concerning the build-process, the code-style or something similar. + +### `Timeline` +Concerns the Timeline-Graph implementation. + + +## Additional labels + +### `Docs` +This issue concerns only the documentation.
+If an existing issue is documented wrongly this is a [`Problem`](#problem) in the component and not a [`docs`](#docs) issue.
+This can be used for typos or requests for an improvement of the docs. + +### `Duplicate` +This issues is a duplicate of an existing issue. The duplicate should be closed. In addition, add a reference to the original issue with a comment. + +### `Fixed awaiting release` +This Issue is fixed or implemented in the "develop" branch but is not released yet and therefore should be still open.
+This issues should be closed after the changes are merged into the "master" branch. + +### `For everyone!` +This is a good issue to start working on if you are new to vis.js and want to help.
+This label is also used for labels that may concern a lot of vis.js users. + +### `IE / Edge` +These issues concern a problem with the Microsoft Internet Explorer or Edge browser.
+ +### `invalid` +This is not a valid issue.
+Someone just created an empty issue, picked the wrong project or something similar.
+This can also be used for pull-request to a non-develop branch or something similar.
+This issue or pull request should be closed immediately. + +### `Issue Inactive` +Issues marked as [`Question`](#question) or [`Problem`](#problem) get marked as inactive when the author is not responsive or the topic is old.
+If an issue is marked as inactive for about 2 weeks it can be closed without any hesitation. + +### `PRIORITY` +In general this is used for major bugs. There should only exist a few issues marked as PRIORITY at the same time.
+These issues need to be handled before all others. + +### `Requires breaking change` +A lot of code needs to be changed to implement this. This is maybe something for a major release or something for someone with a lot of time on their hands :-) + +### `waiting for answer/improvement` +This is mostly used for pull requests were a reviewer requested some changes and the owner has not responded yet. + +### `Work In Progress` +Someone is working on this issue or a pull request already exists and needs to be reviewed.
+ +## Example Workflows + +### Bug + +[`Problem`](#Problem) ⟶ [`Confirmed Bug`](#confirmed-bug) ⟶ [`Work In Progress`](#work-in-progress) ⟶ [`Fixed awaiting release`](#fixed-awaiting-release) + +### Feature-Request + +[`Feature-Request`](#feature-request) ⟶ [`Work In Progress`](#work-in-progress) ⟶ [`Fixed awaiting release`](#fixed-awaiting-release) From 77699b4fd9f321e1923d30766ca220d6414b6028 Mon Sep 17 00:00:00 2001 From: yotamberk Date: Mon, 1 May 2017 10:47:09 +0300 Subject: [PATCH 07/28] Eliminate repeatedly fired `rangechanged` events on mousewheel (#2989) * Fix redraw order * Fix error when option is not defined * Allow template labels * Add .travis.yml file * Add experiment travis code * Fix react example * Add pausing of rangechanged in scroll --- lib/timeline/Range.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/timeline/Range.js b/lib/timeline/Range.js index f568bc58..0ae6dec0 100644 --- a/lib/timeline/Range.js +++ b/lib/timeline/Range.js @@ -196,12 +196,12 @@ Range.prototype.setRange = function(start, end, options, callback) { if (options.byUser !== true) { options.byUser = false; } + var me = this; var finalStart = start != undefined ? util.convert(start, 'Date').valueOf() : null; var finalEnd = end != undefined ? util.convert(end, 'Date').valueOf() : null; this._cancelAnimation(); if (options.animation) { // true or an Object - var me = this; var initStart = this.start; var initEnd = this.end; var duration = (typeof options.animation === 'object' && 'duration' in options.animation) ? options.animation.duration : 500; @@ -265,8 +265,12 @@ Range.prototype.setRange = function(start, end, options, callback) { byUser: options.byUser, event: options.event }; + this.body.emitter.emit('rangechange', params); - this.body.emitter.emit('rangechanged', params); + clearTimeout( me.timeoutID ); + me.timeoutID = setTimeout( function () { + me.body.emitter.emit('rangechanged', params); + }, 200 ); if (callback) { return callback() } } } From 0a64249ce65b1322a2b67ccd055d02c8272109dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Schn=C3=BCriger?= Date: Tue, 2 May 2017 14:07:46 +0200 Subject: [PATCH 08/28] Implementation of a week scale feature (#3009) * Add Gitter badge (#2179) * do not generate source-maps in distribution version * add 'dist' folder for deployment * added Badges * added codeclimate badge * added @Tooa to the support team * added badges from isitmaintained.com (#2517) * do not ignore dist and test folders in master * generated dist files for v4.18.0 * generated dist files for v4.18.1 * Cheap fix for bug #2795 * Update to PR #2826 to use newline format * changed to v4.18.1-SNAPSHOT * chore(docs): general improvements (#2652) * removed NOTICE file * updated license date range to include 2017 * chore(docs): updated support team members * chore: updated dependencies and devDependencies (#2649) * Fixes instanceof Object statements for objects from other windows and iFrames. (#2631) * Replaces instanceof Object checks with typeof to prevent cross tab issues. * Adds missing space. * chore: removed google-analytics from all examples (#2670) * chore(docs): Add note that PRs should be submitted against the `develop` branch (#2623) Related to: https://github.com/almende/vis/pull/2618 Related to: https://github.com/almende/vis/pull/2620 * feat(timeline): Change setCustomTimeTitle title parameter to be a string or a function (#2611) * change setCustomTimeTitle title parameter, Now could be an string or a function * Fixed indent and spacing * feat(timeline): refactor tooltip to only use one dom-element (#2662) * feat(network): Allow for image nodes to have a selected or broken image (#2601) * feat(tests): run mocha tests in travis ci (#2687) * Added showX(YZ)Axis options to Graph3d (#2686) * Added showX(YZ)Axis to Graph3d * Added show_Axis options to docs and playground example * Resolved merge conflict * Added show_Axis options to docs and playground example * fix(build): use babel version compatible with webpack@1.14 (#2693) fixes #2685 * feat(docs): use babel preset2015 for custom builds (#2678) * add link to a mentioned example (#2709) * chore(lint): added support for eslint (#2695) * Trivial typo fix in how_to_help doc. (#2714) * fix(timeline): #2598 Flickering onUpdateTimeTooltip (#2702) * Fix redraw order * Fix error when option is not defined * Allow template labels * Add .travis.yml file * Add experiment travis code * Fix react example * Add animation to onUpdateTooltip * fix(timeline): #778 Tooltip does not work with background items in timeline (#2703) * Fix redraw order * Fix error when option is not defined * Allow template labels * Add .travis.yml file * Add experiment travis code * Fix react example * Make items z-index default to 1 * Add initial tests for Timeline PointItem (#2716) * fix(timeline): #2679 TypeError: Cannot read property 'hasOwnProperty' of null (#2735) * Fix redraw order * Fix error when option is not defined * Allow template labels * Add .travis.yml file * Add experiment travis code * Fix react example * Fix bug in item editable * feat(timeline): #2647 Dynamic rolling mode option (#2705) * Fix redraw order * Fix error when option is not defined * Allow template labels * Add .travis.yml file * Add experiment travis code * Fix react example * Add toggleRollingMode option * Update docs with toggleRollingMode option * fixes timestep next issue (#2732) * feat(timeline): added new locales for french and espanol (#2723) * Fix eslint problem on Travis. (#2744) * fix: Range.js "event" is undeclared (#2749) * fix(timeline): #2720 Problems with option editable (#2743) Clean up and centralise edit status for Timeline Items. * feat(network): Improve the performance of the network layout engine (#2729) * Improve the performance of the network layout engine Short-cut the execution of a number of methods in LayoutEngine to make them handle highly-connected graphs better. * Demonstrations of layouts of large networks * Added support to supply an end to bar charts to have them scale (#2760) * Added support to supply an X2 to bar charts to have them scale * Fixed graph2d stacking issue. It no longer takes into account hidden items * Changed x2 to end per recommendation and added this to the docs * Initial tests for timeline ItemSet. (#2750) Somewhat more complicated setup, associated with the need for a real window. * [Timeline] Modify redraw logic to treat scroll as needing restack. (#2774) This addresses #1982 and #1417. It possibly reduces performance, but correctness seems better. * fix(timeline): #2672 Item events original event (#2704) * Fix redraw order * Fix error when option is not defined * Allow template labels * Add .travis.yml file * Add experiment travis code * Fix react example * Fix events returned from mouse events * Fix example * Rename censor to stringifyObject in example * [timeline] Update examples to use ISOString format. (#2791) Resolves #2790 * [timeline] Update serialization example to use ISOString dates. (#2789) Resolves #2696 * added github templates for issues and pull-requests (#2787) fixes #2418 * feat(timeline): Add item data as argument to the template function (#2799) * Fix regression introduced in #2743. (#2796) * Fix for issue #2536 (#2803) * Fix for issue #2536 * Adjusted documentation for fix. * Adjustments due to review * Grrrrr whitespace * Fixed Travis issue * Cheap fix for bug #2795 * Update to PR #2826 to use newline format * Update to gitignore to reflect changes on remote * clean dist folder * Local gitignore update * Just a first example file for the week scale feature * Allowing sourcemap creation * Initial (non-functional) commit to ensure we insert code at the right places (check TODOs) * Functional, not bug-free version which works with locale aware week numbers. * Locale-aware implementation and simplified major labels to a full year * Trying to make the major labels show the correct start date * Working implementation of week numbers using localization. * removing development leftovers * Updated package.json * Reintagrate package.json from accidental deletion * Updates for package.json * Fixing package.json * Integrate the week numbers feature in the documentation. * Reverting local changes to .gitignore * Reverting local changes * Extending examples to cover the case when 1st day of week and 1st day of month align; Fixing display bug so that week numbers are not repeated in minorLabels * Putting the examples into a loop --- docs/timeline/index.html | 13 ++- examples/timeline/styling/weekStyling.html | 111 +++++++++++++++++++++ lib/timeline/TimeStep.js | 59 ++++++++++- lib/timeline/component/TimeAxis.js | 6 +- lib/timeline/optionsTimeline.js | 6 +- 5 files changed, 187 insertions(+), 8 deletions(-) create mode 100644 examples/timeline/styling/weekStyling.html diff --git a/docs/timeline/index.html b/docs/timeline/index.html index 81e40f97..374b9f97 100644 --- a/docs/timeline/index.html +++ b/docs/timeline/index.html @@ -612,6 +612,7 @@ function (option, path) { hour: 'HH:mm', weekday: 'ddd D', day: 'D', + week: 'w', month: 'MMM', year: 'YYYY' }, @@ -622,6 +623,7 @@ function (option, path) { hour: 'ddd D MMMM', weekday: 'MMMM YYYY', day: 'MMMM YYYY', + week: 'MMMM YYYY', month: 'YYYY', year: '' } @@ -1044,7 +1046,7 @@ function (option, path) { function When moving items on the Timeline, they will be snapped to nice dates like full hours or days, depending on the current scale. The snap function can be replaced with a custom function, or can be set to null to disable snapping. The signature of the snap function is:
function snap(date: Date, scale: string, step: number) : Date or number
- The parameter scale can be can be 'millisecond', 'second', 'minute', 'hour', 'weekday, 'day, 'month, or 'year'. The parameter step is a number like 1, 2, 4, 5. + The parameter scale can be can be 'millisecond', 'second', 'minute', 'hour', 'weekday, 'week', 'day, 'month, or 'year'. The parameter step is a number like 1, 2, 4, 5. @@ -1088,10 +1090,11 @@ function (option, path) { timeAxis.scale String none - Set a fixed scale for the time axis of the Timeline. Choose from 'millisecond', 'second', 'minute', 'hour', 'weekday', 'day', 'month', 'year'. Example usage: + Set a fixed scale for the time axis of the Timeline. Choose from 'millisecond', 'second', 'minute', 'hour', 'weekday', 'week', 'day', 'month', 'year'. Example usage:
var options = {
   timeAxis: {scale: 'minute', step: 5}
 }
+

Note: The 'week' scale only works properly when locales are enabled.

@@ -2023,6 +2026,9 @@ var options = { Daysvis-day1, vis-day2, ..., vis-day31 + + Weekvis-week1, vis-week2, ..., vis-week53 + Monthsvis-january, vis-february, vis-march, vis-april, vis-may, vis-june, vis-july, vis-august, vis-september, vis-october, vis-november, vis-december @@ -2030,6 +2036,9 @@ var options = { Yearsvis-year2014, vis-year2015, ... +

+ Note: the 'week' scale is not included in the automatic zoom levels as its scale is not a direct logical successor of 'days' nor a logical predecessor of 'months' +

Examples:

diff --git a/examples/timeline/styling/weekStyling.html b/examples/timeline/styling/weekStyling.html new file mode 100644 index 00000000..9a3a919e --- /dev/null +++ b/examples/timeline/styling/weekStyling.html @@ -0,0 +1,111 @@ + + + + Timeline | Grid styling + + + + + + + + + + +

+ Week numbers are calculated based on locales. For this to properly work, the timeline must be loaded with a version + of moment.js including locales.

+

To set a locale for the timeline, specify the option + {locale: STRING}.

+

To set the scale to use week numbers, use for example {scale: 'week', step: 1}.

+

The following timeline is initialized with the 'de' locale and items are added with locally localized moment.js + objects. The timeline locale can be switched to another locale at runtime. If you choose the 'en' locale, the week + numbers will be calculated according to the US week calendar numbering scheme.

+ +

+ + +

+ +
+ + + + \ No newline at end of file diff --git a/lib/timeline/TimeStep.js b/lib/timeline/TimeStep.js index e3f6ff7b..530e1726 100644 --- a/lib/timeline/TimeStep.js +++ b/lib/timeline/TimeStep.js @@ -69,6 +69,7 @@ TimeStep.FORMAT = { hour: 'HH:mm', weekday: 'ddd D', day: 'D', + week: 'w', month: 'MMM', year: 'YYYY' }, @@ -79,6 +80,7 @@ TimeStep.FORMAT = { hour: 'ddd D MMMM', weekday: 'MMMM YYYY', day: 'MMMM YYYY', + week: 'MMMM YYYY', month: 'YYYY', year: '' } @@ -101,7 +103,7 @@ TimeStep.prototype.setMoment = function (moment) { /** * Set custom formatting for the minor an major labels of the TimeStep. * Both `minorLabels` and `majorLabels` are an Object with properties: - * 'millisecond', 'second', 'minute', 'hour', 'weekday', 'day', 'month', 'year'. + * 'millisecond', 'second', 'minute', 'hour', 'weekday', 'day', 'week', 'month', 'year'. * @param {{minorLabels: Object, majorLabels: Object}} format */ TimeStep.prototype.setFormat = function (format) { @@ -153,6 +155,7 @@ TimeStep.prototype.roundToMinor = function() { this.current.year(this.step * Math.floor(this.current.year() / this.step)); this.current.month(0); case 'month': this.current.date(1); + case 'week': this.current.weekday(0); case 'day': // intentional fall through case 'weekday': this.current.hours(0); case 'hour': this.current.minutes(0); @@ -170,6 +173,7 @@ TimeStep.prototype.roundToMinor = function() { case 'hour': this.current.subtract(this.current.hours() % this.step, 'hours'); break; case 'weekday': // intentional fall through case 'day': this.current.subtract((this.current.date() - 1) % this.step, 'day'); break; + case 'week': this.current.subtract(this.current.week() % this.step, 'week'); break; case 'month': this.current.subtract(this.current.month() % this.step, 'month'); break; case 'year': this.current.subtract(this.current.year() % this.step, 'year'); break; default: break; @@ -210,6 +214,21 @@ TimeStep.prototype.next = function() { break; case 'weekday': // intentional fall through case 'day': this.current.add(this.step, 'day'); break; + case 'week': + if (this.current.weekday() !== 0){ // we had a month break not correlating with a week's start before + this.current.weekday(0); // switch back to week cycles + this.current.add(this.step, 'week'); + } else { // first day of the week + var nextWeek = this.current.clone(); + nextWeek.add(1, 'week'); + if(nextWeek.isSame(this.current, 'month')){ // is the first day of the next week in the same month? + this.current.add(this.step, 'week'); // the default case + } else { // inject a step at each first day of the month + this.current.add(this.step, 'week'); + this.current.date(1); + } + } + break; case 'month': this.current.add(this.step, 'month'); break; case 'year': this.current.add(this.step, 'year'); break; default: break; @@ -224,6 +243,7 @@ TimeStep.prototype.next = function() { case 'hour': if(this.current.hours() > 0 && this.current.hours() < this.step) this.current.hours(0); break; case 'weekday': // intentional fall through case 'day': if(this.current.date() < this.step+1) this.current.date(1); break; + case 'week': if(this.current.week() < this.step) this.current.week(1); break; // week numbering starts at 1, not 0 case 'month': if(this.current.month() < this.step) this.current.month(0); break; case 'year': break; // nothing to do for year default: break; @@ -260,7 +280,7 @@ TimeStep.prototype.getCurrent = function() { * @param {{scale: string, step: number}} params * An object containing two properties: * - A string 'scale'. Choose from 'millisecond', 'second', - * 'minute', 'hour', 'weekday', 'day', 'month', 'year'. + * 'minute', 'hour', 'weekday', 'day', 'week', 'month', 'year'. * - A number 'step'. A step size, by default 1. * Choose for example 1, 2, 5, or 10. */ @@ -338,7 +358,7 @@ TimeStep.prototype.setMinimumStep = function(minimumStep) { * Static function * @param {Date} date the date to be snapped. * @param {string} scale Current scale, can be 'millisecond', 'second', - * 'minute', 'hour', 'weekday, 'day', 'month', 'year'. + * 'minute', 'hour', 'weekday, 'day', 'week', 'month', 'year'. * @param {number} step Current step (1, 2, 4, 5, ... * @return {Date} snappedDate */ @@ -370,6 +390,20 @@ TimeStep.snap = function(date, scale, step) { clone.seconds(0); clone.milliseconds(0); } + else if (scale == 'week') { + if (clone.weekday() > 2) { // doing it the momentjs locale aware way + clone.weekday(0); + clone.add(1, 'week'); + } + else { + clone.weekday(0); + } + + clone.hours(0); + clone.minutes(0); + clone.seconds(0); + clone.milliseconds(0); + } else if (scale == 'day') { //noinspection FallthroughInSwitchStatementJS switch (step) { @@ -452,6 +486,7 @@ TimeStep.prototype.isMajor = function() { switch (this.scale) { case 'year': case 'month': + case 'week': case 'weekday': case 'day': case 'hour': @@ -465,6 +500,7 @@ TimeStep.prototype.isMajor = function() { } else if (this.switchedMonth == true) { switch (this.scale) { + case 'week': case 'weekday': case 'day': case 'hour': @@ -501,6 +537,8 @@ TimeStep.prototype.isMajor = function() { case 'weekday': // intentional fall through case 'day': return (date.date() == 1); + case 'week': + return (date.date() == 1); case 'month': return (date.month() == 0); case 'year': @@ -530,7 +568,15 @@ TimeStep.prototype.getLabelMinor = function(date) { } var format = this.format.minorLabels[this.scale]; - return (format && format.length > 0) ? this.moment(date).format(format) : ''; + // noinspection FallThroughInSwitchStatementJS + switch (this.scale) { + case 'week': + if(this.isMajor() && date.weekday() !== 0){ + return ""; + } + default: + return (format && format.length > 0) ? this.moment(date).format(format) : ''; + } }; /** @@ -624,6 +670,11 @@ TimeStep.prototype.getClassName = function() { classNames.push(this.step <= 2 ? 'vis-' + current.format('dddd').toLowerCase() : ''); classNames.push(even(current.date() - 1)); break; + case 'week': + classNames.push('vis-week' + current.format('w')); + classNames.push(currentWeek(current)); + classNames.push(even(current.week())); + break; case 'month': classNames.push('vis-' + current.format('MMMM').toLowerCase()); classNames.push(currentMonth(current)); diff --git a/lib/timeline/component/TimeAxis.js b/lib/timeline/component/TimeAxis.js index f575af4b..2d1b1d18 100644 --- a/lib/timeline/component/TimeAxis.js +++ b/lib/timeline/component/TimeAxis.js @@ -227,6 +227,7 @@ TimeAxis.prototype._repaintLabels = function () { var x; var xNext; var isMajor, nextIsMajor; + var showMinorGrid; var width = 0, prevWidth; var line; var labelMinor; @@ -255,7 +256,10 @@ TimeAxis.prototype._repaintLabels = function () { prevWidth = width; width = xNext - x; - var showMinorGrid = (width >= prevWidth * 0.4); // prevent displaying of the 31th of the month on a scale of 5 days + switch (step.scale) { + case 'week': showMinorGrid = true; break; + default: showMinorGrid = (width >= prevWidth * 0.4); break; // prevent displaying of the 31th of the month on a scale of 5 days + } if (this.options.showMinorLabels && showMinorGrid) { var label = this._repaintMinorText(x, labelMinor, orientation, className); diff --git a/lib/timeline/optionsTimeline.js b/lib/timeline/optionsTimeline.js index 41ac8d0b..fb6f3c3d 100644 --- a/lib/timeline/optionsTimeline.js +++ b/lib/timeline/optionsTimeline.js @@ -54,6 +54,7 @@ let allOptions = { hour: {string,'undefined': 'undefined'}, weekday: {string,'undefined': 'undefined'}, day: {string,'undefined': 'undefined'}, + week: {string,'undefined': 'undefined'}, month: {string,'undefined': 'undefined'}, year: {string,'undefined': 'undefined'}, __type__: {object, 'function': 'function'} @@ -65,6 +66,7 @@ let allOptions = { hour: {string,'undefined': 'undefined'}, weekday: {string,'undefined': 'undefined'}, day: {string,'undefined': 'undefined'}, + week: {string,'undefined': 'undefined'}, month: {string,'undefined': 'undefined'}, year: {string,'undefined': 'undefined'}, __type__: {object, 'function': 'function'} @@ -181,6 +183,7 @@ let configureOptions = { hour: 'HH:mm', weekday: 'ddd D', day: 'D', + week: 'w', month: 'MMM', year: 'YYYY' }, @@ -191,6 +194,7 @@ let configureOptions = { hour: 'ddd D MMMM', weekday: 'MMMM YYYY', day: 'MMMM YYYY', + week: 'MMMM YYYY', month: 'YYYY', year: '' } @@ -236,7 +240,7 @@ let configureOptions = { start: '', //template: {'function': 'function'}, //timeAxis: { - // scale: ['millisecond', 'second', 'minute', 'hour', 'weekday', 'day', 'month', 'year'], + // scale: ['millisecond', 'second', 'minute', 'hour', 'weekday', 'day', 'week', 'month', 'year'], // step: [1, 1, 10, 1] //}, tooltip: { From 41677bdbf6eba60f6aabecaff7c5bb8d0b59cb37 Mon Sep 17 00:00:00 2001 From: wimrijnders Date: Wed, 3 May 2017 09:40:53 +0200 Subject: [PATCH 09/28] Refactoring in Canvas.js (#3030) --- lib/network/modules/Canvas.js | 46 ++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/lib/network/modules/Canvas.js b/lib/network/modules/Canvas.js index c378c183..ee2fd387 100644 --- a/lib/network/modules/Canvas.js +++ b/lib/network/modules/Canvas.js @@ -189,12 +189,7 @@ class Canvas { } else { let ctx = this.frame.canvas.getContext("2d"); - this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || - ctx.mozBackingStorePixelRatio || - ctx.msBackingStorePixelRatio || - ctx.oBackingStorePixelRatio || - ctx.backingStorePixelRatio || 1); - + this._setPixelRatio(ctx); this.frame.canvas.getContext("2d").setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); } @@ -264,11 +259,7 @@ class Canvas { // update the pixel ratio let ctx = this.frame.canvas.getContext("2d"); let previousRatio = this.pixelRatio; // we cache this because the camera state storage needs the old value - this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || - ctx.mozBackingStorePixelRatio || - ctx.msBackingStorePixelRatio || - ctx.oBackingStorePixelRatio || - ctx.backingStorePixelRatio || 1); + this._setPixelRatio(ctx); if (width != this.options.width || height != this.options.height || this.frame.style.width != width || this.frame.style.height != height) { this._getCameraState(previousRatio); @@ -296,26 +287,29 @@ class Canvas { // this would adapt the width of the canvas to the width from 100% if and only if // there is a change. + let newWidth = Math.round(this.frame.canvas.clientWidth * this.pixelRatio); + let newHeight = Math.round(this.frame.canvas.clientHeight * this.pixelRatio); + // store the camera if there is a change in size. - if (this.frame.canvas.width != Math.round(this.frame.canvas.clientWidth * this.pixelRatio) || this.frame.canvas.height != Math.round(this.frame.canvas.clientHeight * this.pixelRatio)) { + if (this.frame.canvas.width !== newWidth || this.frame.canvas.height !== newHeight) { this._getCameraState(previousRatio); } - if (this.frame.canvas.width != Math.round(this.frame.canvas.clientWidth * this.pixelRatio)) { - this.frame.canvas.width = Math.round(this.frame.canvas.clientWidth * this.pixelRatio); + if (this.frame.canvas.width !== newWidth) { + this.frame.canvas.width = newWidth; emitEvent = true; } - if (this.frame.canvas.height != Math.round(this.frame.canvas.clientHeight * this.pixelRatio)) { - this.frame.canvas.height = Math.round(this.frame.canvas.clientHeight * this.pixelRatio); + if (this.frame.canvas.height !== newHeight) { + this.frame.canvas.height = newHeight; emitEvent = true; } } if (emitEvent === true) { this.body.emitter.emit('resize', { - width:Math.round(this.frame.canvas.width / this.pixelRatio), - height:Math.round(this.frame.canvas.height / this.pixelRatio), - oldWidth: Math.round(oldWidth / this.pixelRatio), + width : Math.round(this.frame.canvas.width / this.pixelRatio), + height : Math.round(this.frame.canvas.height / this.pixelRatio), + oldWidth : Math.round(oldWidth / this.pixelRatio), oldHeight: Math.round(oldHeight / this.pixelRatio) }); @@ -330,6 +324,18 @@ class Canvas { }; + /** + * @private + */ + _setPixelRatio(ctx) { + this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || + ctx.mozBackingStorePixelRatio || + ctx.msBackingStorePixelRatio || + ctx.oBackingStorePixelRatio || + ctx.backingStorePixelRatio || 1); + } + + /** * Convert the X coordinate in DOM-space (coordinate point in browser relative to the container div) to * the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) @@ -397,4 +403,4 @@ class Canvas { } -export default Canvas; \ No newline at end of file +export default Canvas; From 10927b5931348b90cf7ce5b83fe74d160cd6a816 Mon Sep 17 00:00:00 2001 From: yotamberk Date: Wed, 3 May 2017 14:56:45 +0300 Subject: [PATCH 10/28] Fix click and doubleclick events on items (#2988) * Fix redraw order * Fix error when option is not defined * Allow template labels * Add .travis.yml file * Add experiment travis code * Fix react example * Fix item click and doubleclick * Remove new empty line * Return id of item tapped * Fix comments from Review * Add missing semicolon --- lib/timeline/component/item/Item.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/timeline/component/item/Item.js b/lib/timeline/component/item/Item.js index d7269bae..30c0bbc8 100644 --- a/lib/timeline/component/item/Item.js +++ b/lib/timeline/component/item/Item.js @@ -146,15 +146,25 @@ Item.prototype.repositionY = function() { Item.prototype._repaintDragCenter = function () { if (this.selected && this.options.editable.updateTime && !this.dom.dragCenter) { var me = this; - // create and show drag area var dragCenter = document.createElement('div'); dragCenter.className = 'vis-drag-center'; dragCenter.dragCenterItem = this; + var hammer = new Hammer(dragCenter); - new Hammer(dragCenter).on('doubletap', function (event) { + hammer.on('tap', function (event) { + me.parent.itemSet.body.emitter.emit('click', { + event: event, + item: me.id + }); + }); + hammer.on('doubletap', function (event) { event.stopPropagation(); me.parent.itemSet._onUpdateItem(me); + me.parent.itemSet.body.emitter.emit('doubleClick', { + event: event, + item: me.id + }); }); if (this.dom.box) { @@ -370,7 +380,6 @@ Item.prototype._updateContents = function (element) { throw new Error('Property "content" missing in item ' + this.id); } } - this.content = content; } } From a58b92c1233e7ed8d708bc0ef8f4744326e0aeb5 Mon Sep 17 00:00:00 2001 From: wimrijnders Date: Fri, 5 May 2017 08:29:08 +0200 Subject: [PATCH 11/28] Fix handling of node id's in saveAndLoad example (#2943) * Fix handling of node id's in saveAndLoad example * Code cleanup * Fixes for review --- examples/network/other/saveAndLoad.html | 42 +++++++++++++++---------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/examples/network/other/saveAndLoad.html b/examples/network/other/saveAndLoad.html index ee71b528..6f96df34 100644 --- a/examples/network/other/saveAndLoad.html +++ b/examples/network/other/saveAndLoad.html @@ -71,15 +71,6 @@ draw(); } - function addContextualInformation(elem, index, array) { - addId(elem, index); - addConnections(elem, index); - } - - function addId(elem, index) { - elem.id = index; - } - function addConnections(elem, index) { // need to replace this with a tree of the network, then get child direct children of the element elem.connections = network.getConnectedNodes(index); @@ -107,7 +98,7 @@ var nodes = objectToArray(network.getPositions()); - nodes.forEach(addContextualInformation); + nodes.forEach(addConnections); // pretty print node data var exportValue = JSON.stringify(nodes, undefined, 2); @@ -141,30 +132,47 @@ return new vis.DataSet(networkNodes); } + function getNodeById(data, id) { + for (var n = 0; n < data.length; n++) { + if (data[n].id == id) { // double equals since id can be numeric or string + return data[n]; + } + }; + + throw 'Can not find id \'' + id + '\' in data'; + } + function getEdgeData(data) { var networkEdges = []; - data.forEach(function(node, index, array) { + data.forEach(function(node) { // add the connection node.connections.forEach(function(connId, cIndex, conns) { networkEdges.push({from: node.id, to: connId}); + let cNode = getNodeById(data, connId); - var elementConnections = array[connId].connections; + var elementConnections = cNode.connections; // remove the connection from the other node to prevent duplicate connections var duplicateIndex = elementConnections.findIndex(function(connection) { - connection === node.id; + return connection == node.id; // double equals since id can be numeric or string }); - elementConnections = elementConnections.splice(0, duplicateIndex - 1).concat(elementConnections.splice(duplicateIndex + 1, elementConnections.length)) - }); + + if (duplicateIndex != -1) { + elementConnections.splice(duplicateIndex, 1); + }; + }); }); return new vis.DataSet(networkEdges); } function objectToArray(obj) { - return Object.keys(obj).map(function (key) { return obj[key]; }); + return Object.keys(obj).map(function (key) { + obj[key].id = key; + return obj[key]; + }); } function resizeExportArea() { @@ -174,4 +182,4 @@ init(); - \ No newline at end of file + From 29e696aaad24ef8a636972c9b1d2c55e9a3278b1 Mon Sep 17 00:00:00 2001 From: wimrijnders Date: Sat, 13 May 2017 21:25:01 +0200 Subject: [PATCH 12/28] Label.getFormattingValues() fix option fallback to main font for mod-fonts (#3054) --- .../modules/components/shared/Label.js | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/lib/network/modules/components/shared/Label.js b/lib/network/modules/components/shared/Label.js index c5cb3fb1..80afc804 100644 --- a/lib/network/modules/components/shared/Label.js +++ b/lib/network/modules/components/shared/Label.js @@ -717,12 +717,26 @@ class Label { } getFormattingValues(ctx, selected, hover, mod) { + var getValue = function(fontOptions, mod, option) { + if (mod === "normal") { + if (option === 'mod' ) return ""; + return fontOptions[option]; + } + + if (fontOptions[mod][option]) { + return fontOptions[mod][option]; + } else { + // Take from parent font option + return fontOptions[option]; + } + }; + let values = { - color: (mod === "normal") ? this.fontOptions.color : this.fontOptions[mod].color, - size: (mod === "normal") ? this.fontOptions.size : this.fontOptions[mod].size, - face: (mod === "normal") ? this.fontOptions.face : this.fontOptions[mod].face, - mod: (mod === "normal") ? "" : this.fontOptions[mod].mod, - vadjust: (mod === "normal") ? this.fontOptions.vadjust : this.fontOptions[mod].vadjust, + color : getValue(this.fontOptions, mod, 'color' ), + size : getValue(this.fontOptions, mod, 'size' ), + face : getValue(this.fontOptions, mod, 'face' ), + mod : getValue(this.fontOptions, mod, 'mod' ), + vadjust: getValue(this.fontOptions, mod, 'vadjust'), strokeWidth: this.fontOptions.strokeWidth, strokeColor: this.fontOptions.strokeColor }; From 167398063e7c50955981f06f5dce07dd480bbb02 Mon Sep 17 00:00:00 2001 From: wimrijnders Date: Sat, 13 May 2017 21:29:19 +0200 Subject: [PATCH 13/28] Set CircleImageBase.imageObjAlt always when options change (#3053) --- .../modules/components/nodes/util/CircleImageBase.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/network/modules/components/nodes/util/CircleImageBase.js b/lib/network/modules/components/nodes/util/CircleImageBase.js index 9d90268b..99bff14a 100644 --- a/lib/network/modules/components/nodes/util/CircleImageBase.js +++ b/lib/network/modules/components/nodes/util/CircleImageBase.js @@ -26,12 +26,12 @@ class CircleImageBase extends NodeBase { } setImages(imageObj, imageObjAlt) { - if (imageObj) { - this.imageObj = imageObj; - - if (imageObjAlt) { - this.imageObjAlt = imageObjAlt; - } + if (imageObjAlt && this.selected) { + this.imageObj = imageObjAlt; + this.imageObjAlt = imageObj; + } else { + this.imageObj = imageObj; + this.imageObjAlt = imageObjAlt; } } From e422c3424f398a1f414de223b196fdb2cc6807a1 Mon Sep 17 00:00:00 2001 From: wimrijnders Date: Sat, 13 May 2017 21:39:11 +0200 Subject: [PATCH 14/28] Refactoring of Label.propagateFonts() (#3052) * Refactor propagateFonts(), first working version. Default font for 'mono' is now 'monospace' * Tested and cleaned up refactoring changes --- lib/network/modules/NodesHandler.js | 2 +- .../modules/components/shared/Label.js | 234 +++++++++--------- 2 files changed, 124 insertions(+), 112 deletions(-) diff --git a/lib/network/modules/NodesHandler.js b/lib/network/modules/NodesHandler.js index 5ed50e24..7c56f3d2 100644 --- a/lib/network/modules/NodesHandler.js +++ b/lib/network/modules/NodesHandler.js @@ -64,7 +64,7 @@ class NodesHandler { mono: { mod: '', size: 15, // px - face: 'courier new', + face: 'monospace', vadjust: 2 } }, diff --git a/lib/network/modules/components/shared/Label.js b/lib/network/modules/components/shared/Label.js index 80afc804..becbf0d3 100644 --- a/lib/network/modules/components/shared/Label.js +++ b/lib/network/modules/components/shared/Label.js @@ -126,124 +126,136 @@ class Label { } } + + /** + * Collapse the font options for the multi-font to single objects, from + * the chain of option objects passed. + * + * If an option for a specific multi-font is not present, the parent + * option is checked for the given option. + * + * NOTE: naming of 'groupOptions' is a misnomer; the actual value passed + * is the new values to set from setOptions(). + */ propagateFonts(options, groupOptions, defaultOptions) { - if (this.fontOptions.multi) { - let mods = [ 'bold', 'ital', 'boldital', 'mono' ]; - for (const mod of mods) { - let optionsFontMod; - if (options.font) { - optionsFontMod = options.font[mod]; - } - if (typeof optionsFontMod === 'string') { - let modOptionsArray = optionsFontMod.split(" "); - this.fontOptions[mod].size = modOptionsArray[0].replace("px",""); - this.fontOptions[mod].face = modOptionsArray[1]; - this.fontOptions[mod].color = modOptionsArray[2]; - this.fontOptions[mod].vadjust = this.fontOptions.vadjust; - this.fontOptions[mod].mod = defaultOptions.font[mod].mod; - } else { - // We need to be crafty about loading the modded fonts. We want as - // much 'natural' versatility as we can get, so a simple global - // change propagates in an expected way, even if not stictly logical. - - // face: We want to capture any direct settings and overrides, but - // fall back to the base font if there aren't any. We make a - // special exception for mono, since we probably don't want to - // sync to a the base font face. - // - // if the mod face is in the node's options, use it - // else if the mod face is in the global options, use it - // else if the face is in the global options, use it - // else use the base font's face. - if (optionsFontMod && optionsFontMod.hasOwnProperty('face')) { - this.fontOptions[mod].face = optionsFontMod.face; - } else if (groupOptions.font && groupOptions.font[mod] && - groupOptions.font[mod].hasOwnProperty('face')) { - this.fontOptions[mod].face = groupOptions.font[mod].face; - } else if (mod === 'mono') { - this.fontOptions[mod].face = defaultOptions.font[mod].face; - } else if (groupOptions.font && - groupOptions.font.hasOwnProperty('face')) { - this.fontOptions[mod].face = groupOptions.font.face; - } else { - this.fontOptions[mod].face = this.fontOptions.face; - } + if (!this.fontOptions.multi) return; - // color: this is handled just like the face. - if (optionsFontMod && optionsFontMod.hasOwnProperty('color')) { - this.fontOptions[mod].color = optionsFontMod.color; - } else if (groupOptions.font && groupOptions.font[mod] && - groupOptions.font[mod].hasOwnProperty('color')) { - this.fontOptions[mod].color = groupOptions.font[mod].color; - } else if (groupOptions.font && - groupOptions.font.hasOwnProperty('color')) { - this.fontOptions[mod].color = groupOptions.font.color; - } else { - this.fontOptions[mod].color = this.fontOptions.color; - } + /** + * Get property value from options.font[mod][property] if present. + * If mod not passed, use property value from options.font[property]. + * + * @return value if found, null otherwise. + */ + var getP = function(options, mod, property) { + if (!options.font) return null; - // mod: this is handled just like the face, except we never grab the - // base font's mod. We know they're in the defaultOptions, and unless - // we've been steered away from them, we use the default. - if (optionsFontMod && optionsFontMod.hasOwnProperty('mod')) { - this.fontOptions[mod].mod = optionsFontMod.mod; - } else if (groupOptions.font && groupOptions.font[mod] && - groupOptions.font[mod].hasOwnProperty('mod')) { - this.fontOptions[mod].mod = groupOptions.font[mod].mod; - } else if (groupOptions.font && - groupOptions.font.hasOwnProperty('mod')) { - this.fontOptions[mod].mod = groupOptions.font.mod; - } else { - this.fontOptions[mod].mod = defaultOptions.font[mod].mod; - } + var opt = options.font; - // size: It's important that we size up defaults similarly if we're - // using default faces unless overriden. We want to preserve the - // ratios closely - but if faces have changed, all bets are off. - // - // if the mod size is in the node's options, use it - // else if the mod size is in the global options, use it - // else if the mod face is the same as the default and the base face - // is the same as the default, scale the mod size using the same - // ratio - // else if the size is in the global options, use it - // else use the base font's size. - if (optionsFontMod && optionsFontMod.hasOwnProperty('size')) { - this.fontOptions[mod].size = optionsFontMod.size; - } else if (groupOptions.font && groupOptions.font[mod] && - groupOptions.font[mod].hasOwnProperty('size')) { - this.fontOptions[mod].size = groupOptions.font[mod].size; - } else if ((this.fontOptions[mod].face === defaultOptions.font[mod].face) && - (this.fontOptions.face === defaultOptions.font.face)) { - let ratio = this.fontOptions.size / Number(defaultOptions.font.size); - this.fontOptions[mod].size = defaultOptions.font[mod].size * ratio; - } else if (groupOptions.font && - groupOptions.font.hasOwnProperty('size')) { - this.fontOptions[mod].size = groupOptions.font.size; - } else { - this.fontOptions[mod].size = this.fontOptions.size; - } + if (mod) { + if (!opt[mod]) return null; + opt = opt[mod]; + } - // vadjust: this is handled just like the size. - if (optionsFontMod && optionsFontMod.hasOwnProperty('vadjust')) { - this.fontOptions[mod].vadjust = optionsFontMod.vadjust; - } else if (groupOptions.font && - groupOptions.font[mod] && groupOptions.font[mod].hasOwnProperty('vadjust')) { - this.fontOptions[mod].vadjust = groupOptions.font[mod].vadjust; - } else if ((this.fontOptions[mod].face === defaultOptions.font[mod].face) && - (this.fontOptions.face === defaultOptions.font.face)) { - let ratio = this.fontOptions.size / Number(defaultOptions.font.size); - this.fontOptions[mod].vadjust = defaultOptions.font[mod].vadjust * Math.round(ratio); - } else if (groupOptions.font && - groupOptions.font.hasOwnProperty('vadjust')) { - this.fontOptions[mod].vadjust = groupOptions.font.vadjust; - } else { - this.fontOptions[mod].vadjust = this.fontOptions.vadjust; - } + if (opt.hasOwnProperty(property)) { + return opt[property]; + } + + return null; + }; + + + let mods = [ 'bold', 'ital', 'boldital', 'mono' ]; + for (const mod of mods) { + let modOptions = this.fontOptions[mod]; + let modDefaults = defaultOptions.font[mod]; + + let optionsFontMod; + if (options.font) { + optionsFontMod = options.font[mod]; + } + + if (typeof optionsFontMod === 'string') { + let modOptionsArray = optionsFontMod.split(" "); + modOptions.size = modOptionsArray[0].replace("px",""); + modOptions.face = modOptionsArray[1]; + modOptions.color = modOptionsArray[2]; + modOptions.vadjust = this.fontOptions.vadjust; + modOptions.mod = modDefaults.mod; + } else { + + // We need to be crafty about loading the modded fonts. We want as + // much 'natural' versatility as we can get, so a simple global + // change propagates in an expected way, even if not stictly logical. + + // face: We want to capture any direct settings and overrides, but + // fall back to the base font if there aren't any. We make a + // special exception for mono, since we probably don't want to + // sync to a the base font face. + modOptions.face = + getP(options , mod, 'face') || + getP(groupOptions, mod, 'face') || + (mod === 'mono'? modDefaults.face:null ) || + getP(groupOptions, null, 'face') || + this.fontOptions.face + ; + + // color: this is handled just like the face, except for specific default for 'mono' + modOptions.color = + getP(options , mod, 'color') || + getP(groupOptions, mod, 'color') || + getP(groupOptions, null, 'color') || + this.fontOptions.color + ; + + // mod: this is handled just like the face, except we never grab the + // base font's mod. We know they're in the defaultOptions, and unless + // we've been steered away from them, we use the default. + modOptions.mod = + getP(options , mod, 'mod') || + getP(groupOptions, mod, 'mod') || + getP(groupOptions, null, 'mod') || + modDefaults.mod + ; + + + // It's important that we size up defaults similarly if we're + // using default faces unless overriden. We want to preserve the + // ratios closely - but if faces have changed, all bets are off. + let ratio; + + // NOTE: Following condition always fails, because modDefaults + // has no explicit font property. This is deliberate, see + // var's 'NodesHandler.defaultOptions.font[mod]'. + // However, I want to keep the original logic while refactoring; + // it appears to be working fine even if ratio is never set. + // TODO: examine if this is a bug, fix if necessary. + // + if ((modOptions.face === modDefaults.face) && + (this.fontOptions.face === defaultOptions.font.face)) { + + ratio = this.fontOptions.size / Number(defaultOptions.font.size); } - this.fontOptions[mod].size = Number(this.fontOptions[mod].size); - this.fontOptions[mod].vadjust = Number(this.fontOptions[mod].vadjust); + + + modOptions.size = + getP(options , mod, 'size') || + getP(groupOptions, mod, 'size') || + (ratio? modDefaults.size * ratio: null) || // Scale the mod size using the same ratio + getP(groupOptions, null, 'size') || + this.fontOptions.size + ; + + modOptions.vadjust = + getP(options , mod, 'vadjust') || + getP(groupOptions, mod, 'vadjust') || + (ratio? modDefaults.vadjust * Math.round(ratio): null) || // Scale it using the same ratio + this.fontOptions.vadjust + ; + } + + modOptions.size = Number(modOptions.size); + modOptions.vadjust = Number(modOptions.vadjust); } } From b20fe1231caed09e45ff488d2efff2349cecc1e4 Mon Sep 17 00:00:00 2001 From: wimrijnders Date: Sat, 13 May 2017 21:46:02 +0200 Subject: [PATCH 15/28] Fix for exploding directed network, first working version; refactored hierarchical state in LayoutEngine. (#3017) --- lib/network/modules/LayoutEngine.js | 384 ++++++++++++++++++---------- 1 file changed, 248 insertions(+), 136 deletions(-) diff --git a/lib/network/modules/LayoutEngine.js b/lib/network/modules/LayoutEngine.js index 09468955..e2466792 100644 --- a/lib/network/modules/LayoutEngine.js +++ b/lib/network/modules/LayoutEngine.js @@ -3,6 +3,171 @@ let util = require('../../util'); import NetworkUtil from '../NetworkUtil'; + +/** + * Container for derived data on current network, relating to hierarchy. + * + * Local, private class. + * + * TODO: Perhaps move more code for hierarchy state handling to this class. + * Till now, only the required and most obvious has been done. + */ +class HierarchicalStatus { + + constructor() { + this.childrenReference = {}; + this.parentReference = {}; + this.levels = {}; + this.trees = {}; + + this.isTree = false; + } + + + /** + * Add the relation between given nodes to the current state. + */ + addRelation(parentNodeId, childNodeId) { + if (this.childrenReference[parentNodeId] === undefined) { + this.childrenReference[parentNodeId] = []; + } + this.childrenReference[parentNodeId].push(childNodeId); + + if (this.parentReference[childNodeId] === undefined) { + this.parentReference[childNodeId] = []; + } + this.parentReference[childNodeId].push(parentNodeId); + } + + + /** + * Check if the current state is for a tree or forest network. + * + * This is the case if every node has at most one parent. + * + * Pre: parentReference init'ed properly for current network + */ + checkIfTree() { + for (let i in this.parentReference) { + if (this.parentReference[i].length > 1) { + this.isTree = false; + return; + } + } + + this.isTree = true; + } + + + /** + * Ensure level for given id is defined. + * + * Sets level to zero for given node id if not already present + */ + ensureLevel(nodeId) { + if (this.levels[nodeId] === undefined) { + this.levels[nodeId] = 0; + } + } + + + /** + * get the maximum level of a branch. + * + * TODO: Never entered; find a test case to test this! + */ + getMaxLevel(nodeId) { + let accumulator = {}; + + let _getMaxLevel = (nodeId) => { + if (accumulator[nodeId] !== undefined) { + return accumulator[nodeId]; + } + let level = this.levels[nodeId]; + if (this.childrenReference[nodeId]) { + let children = this.childrenReference[nodeId]; + if (children.length > 0) { + for (let i = 0; i < children.length; i++) { + level = Math.max(level,_getMaxLevel(children[i])); + } + } + } + accumulator[nodeId] = level; + return level; + }; + + return _getMaxLevel(nodeId); + } + + + levelDownstream(nodeA, nodeB) { + if (this.levels[nodeB.id] === undefined) { + // set initial level + if (this.levels[nodeA.id] === undefined) { + this.levels[nodeA.id] = 0; + } + // set level + this.levels[nodeB.id] = this.levels[nodeA.id] + 1; + } + } + + + /** + * Small util method to set the minimum levels of the nodes to zero. + */ + setMinLevelToZero(nodes) { + let minLevel = 1e9; + // get the minimum level + for (let nodeId in nodes) { + if (nodes.hasOwnProperty(nodeId)) { + if (this.levels[nodeId] !== undefined) { + minLevel = Math.min(this.levels[nodeId], minLevel); + } + } + } + + // subtract the minimum from the set so we have a range starting from 0 + for (let nodeId in nodes) { + if (nodes.hasOwnProperty(nodeId)) { + if (this.levels[nodeId] !== undefined) { + this.levels[nodeId] -= minLevel; + } + } + } + } + + + /** + * Get the min and max xy-coordinates of a given tree + */ + getTreeSize(nodes, index) { + let min_x = 1e9; + let max_x = -1e9; + let min_y = 1e9; + let max_y = -1e9; + + for (let nodeId in this.trees) { + if (this.trees.hasOwnProperty(nodeId)) { + if (this.trees[nodeId] === index) { + let node = nodes[nodeId]; + min_x = Math.min(node.x, min_x); + max_x = Math.max(node.x, max_x); + min_y = Math.min(node.y, min_y); + max_y = Math.max(node.y, max_y); + } + } + } + + return { + min_x: min_x, + max_x: max_x, + min_y: min_y, + max_y: max_y + }; + } +} + + class LayoutEngine { constructor(body) { this.body = body; @@ -308,11 +473,8 @@ class LayoutEngine { let definedLevel = false; let definedPositions = true; let undefinedLevel = false; - this.hierarchicalLevels = {}; this.lastNodeOnLevel = {}; - this.hierarchicalChildrenReference = {}; - this.hierarchicalParentReference = {}; - this.hierarchicalTrees = {}; + this.hierarchical = new HierarchicalStatus(); this.treeIndex = -1; this.distributionOrdering = {}; @@ -328,7 +490,7 @@ class LayoutEngine { } if (node.options.level !== undefined) { definedLevel = true; - this.hierarchicalLevels[nodeId] = node.options.level; + this.hierarchical.levels[nodeId] = node.options.level; } else { undefinedLevel = true; @@ -358,9 +520,7 @@ class LayoutEngine { // fallback for cases where there are nodes but no edges for (let nodeId in this.body.nodes) { if (this.body.nodes.hasOwnProperty(nodeId)) { - if (this.hierarchicalLevels[nodeId] === undefined) { - this.hierarchicalLevels[nodeId] = 0; - } + this.hierarchical.ensureLevel(nodeId); } } // check the distribution of the nodes per level. @@ -402,9 +562,9 @@ class LayoutEngine { // shift a single tree by an offset let shiftTree = (index, offset) => { - for (let nodeId in this.hierarchicalTrees) { - if (this.hierarchicalTrees.hasOwnProperty(nodeId)) { - if (this.hierarchicalTrees[nodeId] === index) { + for (let nodeId in this.hierarchical.trees) { + if (this.hierarchical.trees.hasOwnProperty(nodeId)) { + if (this.hierarchical.trees[nodeId] === index) { let node = this.body.nodes[nodeId]; let pos = this._getPositionForHierarchy(node); this._setPositionForHierarchy(node, pos + offset, undefined, true); @@ -415,18 +575,12 @@ class LayoutEngine { // get the width of a tree let getTreeSize = (index) => { - let min = 1e9; - let max = -1e9; - for (let nodeId in this.hierarchicalTrees) { - if (this.hierarchicalTrees.hasOwnProperty(nodeId)) { - if (this.hierarchicalTrees[nodeId] === index) { - let pos = this._getPositionForHierarchy(this.body.nodes[nodeId]); - min = Math.min(pos, min); - max = Math.max(pos, max); - } - } + let res = this.hierarchical.getTreeSize(this.body.nodes, index); + if (this._isVertical()) { + return {min: res.min_x, max: res.max_x}; + } else { + return {min: res.min_y, max: res.max_y}; } - return {min:min, max:max}; }; // get the width of all trees @@ -445,8 +599,8 @@ class LayoutEngine { return; } map[source.id] = true; - if (this.hierarchicalChildrenReference[source.id]) { - let children = this.hierarchicalChildrenReference[source.id]; + if (this.hierarchical.childrenReference[source.id]) { + let children = this.hierarchical.childrenReference[source.id]; if (children.length > 0) { for (let i = 0; i < children.length; i++) { getBranchNodes(this.body.nodes[children[i]], map); @@ -465,7 +619,7 @@ class LayoutEngine { for (let branchNode in branchMap) { if (branchMap.hasOwnProperty(branchNode)) { let node = this.body.nodes[branchNode]; - let level = this.hierarchicalLevels[node.id]; + let level = this.hierarchical.levels[node.id]; let position = this._getPositionForHierarchy(node); // get the space around the node. @@ -484,39 +638,18 @@ class LayoutEngine { return [min, max, minSpace, maxSpace]; }; - // get the maximum level of a branch. - let getMaxLevel = (nodeId) => { - let accumulator = {}; - let _getMaxLevel = (nodeId) => { - if (accumulator[nodeId] !== undefined) { - return accumulator[nodeId]; - } - let level = this.hierarchicalLevels[nodeId]; - if (this.hierarchicalChildrenReference[nodeId]) { - let children = this.hierarchicalChildrenReference[nodeId]; - if (children.length > 0) { - for (let i = 0; i < children.length; i++) { - level = Math.max(level,_getMaxLevel(children[i])); - } - } - } - accumulator[nodeId] = level; - return level; - }; - return _getMaxLevel(nodeId); - }; // check what the maximum level is these nodes have in common. let getCollisionLevel = (node1, node2) => { - let maxLevel1 = getMaxLevel(node1.id); - let maxLevel2 = getMaxLevel(node2.id); + let maxLevel1 = this.hierarchical.getMaxLevel(node1.id); + let maxLevel2 = this.hierarchical.getMaxLevel(node2.id); return Math.min(maxLevel1, maxLevel2); }; // check if two nodes have the same parent(s) let hasSameParent = (node1, node2) => { - let parents1 = this.hierarchicalParentReference[node1.id]; - let parents2 = this.hierarchicalParentReference[node2.id]; + let parents1 = this.hierarchical.parentReference[node1.id]; + let parents2 = this.hierarchical.parentReference[node2.id]; if (parents1 === undefined || parents2 === undefined) { return false; } @@ -539,7 +672,7 @@ class LayoutEngine { if (levelNodes.length > 1) { for (let j = 0; j < levelNodes.length - 1; j++) { if (hasSameParent(levelNodes[j],levelNodes[j+1]) === true) { - if (this.hierarchicalTrees[levelNodes[j].id] === this.hierarchicalTrees[levelNodes[j+1].id]) { + if (this.hierarchical.trees[levelNodes[j].id] === this.hierarchical.trees[levelNodes[j+1].id]) { callback(levelNodes[j],levelNodes[j+1], centerParents); } }} @@ -593,7 +726,7 @@ class LayoutEngine { // console.log("ts",node.id); let nodeId = node.id; let allEdges = node.edges; - let nodeLevel = this.hierarchicalLevels[node.id]; + let nodeLevel = this.hierarchical.levels[node.id]; // gather constants let C2 = this.options.hierarchical.levelSeparation * this.options.hierarchical.levelSeparation; @@ -604,7 +737,7 @@ class LayoutEngine { if (edge.toId != edge.fromId) { let otherNode = edge.toId == nodeId ? edge.from : edge.to; referenceNodes[allEdges[i].id] = otherNode; - if (this.hierarchicalLevels[otherNode.id] < nodeLevel) { + if (this.hierarchical.levels[otherNode.id] < nodeLevel) { aboveEdges.push(edge); } } @@ -802,7 +935,7 @@ class LayoutEngine { if (map === undefined) { useMap = false; } - let level = this.hierarchicalLevels[node.id]; + let level = this.hierarchical.levels[node.id]; if (level !== undefined) { let index = this.distributionIndex[node.id]; let position = this._getPositionForHierarchy(node); @@ -837,16 +970,16 @@ class LayoutEngine { * @private */ _centerParent(node) { - if (this.hierarchicalParentReference[node.id]) { - let parents = this.hierarchicalParentReference[node.id]; + if (this.hierarchical.parentReference[node.id]) { + let parents = this.hierarchical.parentReference[node.id]; for (var i = 0; i < parents.length; i++) { let parentId = parents[i]; let parentNode = this.body.nodes[parentId]; - if (this.hierarchicalChildrenReference[parentId]) { + if (this.hierarchical.childrenReference[parentId]) { // get the range of the children let minPos = 1e9; let maxPos = -1e9; - let children = this.hierarchicalChildrenReference[parentId]; + let children = this.hierarchical.childrenReference[parentId]; if (children.length > 0) { for (let i = 0; i < children.length; i++) { let childNode = this.body.nodes[children[i]]; @@ -893,7 +1026,7 @@ class LayoutEngine { // we get the X or Y values we need and store them in pos and previousPos. The get and set make sure we get X or Y if (handledNodeCount > 0) {pos = this._getPositionForHierarchy(nodeArray[i-1]) + this.options.hierarchical.nodeSpacing;} this._setPositionForHierarchy(node, pos, level); - this._validataPositionAndContinue(node, level, pos); + this._validatePositionAndContinue(node, level, pos); handledNodeCount++; } @@ -913,14 +1046,14 @@ class LayoutEngine { */ _placeBranchNodes(parentId, parentLevel) { // if this is not a parent, cancel the placing. This can happen with multiple parents to one child. - if (this.hierarchicalChildrenReference[parentId] === undefined) { + if (this.hierarchical.childrenReference[parentId] === undefined) { return; } // get a list of childNodes let childNodes = []; - for (let i = 0; i < this.hierarchicalChildrenReference[parentId].length; i++) { - childNodes.push(this.body.nodes[this.hierarchicalChildrenReference[parentId][i]]); + for (let i = 0; i < this.hierarchical.childrenReference[parentId].length; i++) { + childNodes.push(this.body.nodes[this.hierarchical.childrenReference[parentId][i]]); } // use the positions to order the nodes. @@ -929,7 +1062,7 @@ class LayoutEngine { // position the childNodes for (let i = 0; i < childNodes.length; i++) { let childNode = childNodes[i]; - let childNodeLevel = this.hierarchicalLevels[childNode.id]; + let childNodeLevel = this.hierarchical.levels[childNode.id]; // check if the child node is below the parent node and if it has already been positioned. if (childNodeLevel > parentLevel && this.positionedNodes[childNode.id] === undefined) { // get the amount of space required for this node. If parent the width is based on the amount of children. @@ -939,7 +1072,7 @@ class LayoutEngine { if (i === 0) {pos = this._getPositionForHierarchy(this.body.nodes[parentId]);} else {pos = this._getPositionForHierarchy(childNodes[i-1]) + this.options.hierarchical.nodeSpacing;} this._setPositionForHierarchy(childNode, pos, childNodeLevel); - this._validataPositionAndContinue(childNode, childNodeLevel, pos); + this._validatePositionAndContinue(childNode, childNodeLevel, pos); } else { return; @@ -966,7 +1099,11 @@ class LayoutEngine { * @param pos * @private */ - _validataPositionAndContinue(node, level, pos) { + _validatePositionAndContinue(node, level, pos) { + // This only works for strict hierarchical networks, i.e. trees and forests + // Early exit if this is not the case + if (!this.hierarchical.isTree) return; + // if overlap has been detected, we shift the branch if (this.lastNodeOnLevel[level] !== undefined) { let previousPos = this._getPositionForHierarchy(this.body.nodes[this.lastNodeOnLevel[level]]); @@ -1013,7 +1150,7 @@ class LayoutEngine { for (nodeId in this.body.nodes) { if (this.body.nodes.hasOwnProperty(nodeId)) { node = this.body.nodes[nodeId]; - let level = this.hierarchicalLevels[nodeId] === undefined ? 0 : this.hierarchicalLevels[nodeId]; + let level = this.hierarchical.levels[nodeId] === undefined ? 0 : this.hierarchical.levels[nodeId]; if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') { node.y = this.options.hierarchical.levelSeparation * level; node.options.fixed.y = true; @@ -1043,7 +1180,7 @@ class LayoutEngine { for (let nodeId in this.body.nodes) { if (this.body.nodes.hasOwnProperty(nodeId)) { let node = this.body.nodes[nodeId]; - if (this.hierarchicalLevels[nodeId] === undefined) { + if (this.hierarchical.levels[nodeId] === undefined) { hubSize = node.edges.length < hubSize ? hubSize : node.edges.length; } } @@ -1062,15 +1199,8 @@ class LayoutEngine { let hubSize = 1; let levelDownstream = (nodeA, nodeB) => { - if (this.hierarchicalLevels[nodeB.id] === undefined) { - // set initial level - if (this.hierarchicalLevels[nodeA.id] === undefined) { - this.hierarchicalLevels[nodeA.id] = 0; - } - // set level - this.hierarchicalLevels[nodeB.id] = this.hierarchicalLevels[nodeA.id] + 1; - } - }; + this.hierarchical.levelDownstream(nodeA, nodeB); + } while (hubSize > 0) { // determine hubs @@ -1089,8 +1219,11 @@ class LayoutEngine { } } + /** * TODO: release feature + * TODO: Determine if this feature is needed at all + * * @private */ _determineLevelsCustomCallback() { @@ -1101,10 +1234,12 @@ class LayoutEngine { }; + // TODO: perhaps move to HierarchicalStatus. + // But I currently don't see the point, this method is not used. let levelByDirection = (nodeA, nodeB, edge) => { - let levelA = this.hierarchicalLevels[nodeA.id]; + let levelA = this.hierarchical.levels[nodeA.id]; // set initial level - if (levelA === undefined) {this.hierarchicalLevels[nodeA.id] = minLevel;} + if (levelA === undefined) {this.hierarchical.levels[nodeA.id] = minLevel;} let diff = customCallback( NetworkUtil.cloneOptions(nodeA,'node'), @@ -1112,11 +1247,11 @@ class LayoutEngine { NetworkUtil.cloneOptions(edge,'edge') ); - this.hierarchicalLevels[nodeB.id] = this.hierarchicalLevels[nodeA.id] + diff; + this.hierarchical.levels[nodeB.id] = this.hierarchical.levels[nodeA.id] + diff; }; this._crawlNetwork(levelByDirection); - this._setMinLevelToZero(); + this.hierarchical.setMinLevelToZero(this.body.nodes); } /** @@ -1127,45 +1262,21 @@ class LayoutEngine { */ _determineLevelsDirected() { let minLevel = 10000; + let levelByDirection = (nodeA, nodeB, edge) => { - let levelA = this.hierarchicalLevels[nodeA.id]; + let levelA = this.hierarchical.levels[nodeA.id]; // set initial level - if (levelA === undefined) {this.hierarchicalLevels[nodeA.id] = minLevel;} + if (levelA === undefined) {this.hierarchical.levels[nodeA.id] = minLevel;} if (edge.toId == nodeB.id) { - this.hierarchicalLevels[nodeB.id] = this.hierarchicalLevels[nodeA.id] + 1; + this.hierarchical.levels[nodeB.id] = this.hierarchical.levels[nodeA.id] + 1; } else { - this.hierarchicalLevels[nodeB.id] = this.hierarchicalLevels[nodeA.id] - 1; + this.hierarchical.levels[nodeB.id] = this.hierarchical.levels[nodeA.id] - 1; } }; - this._crawlNetwork(levelByDirection); - this._setMinLevelToZero(); - } - - /** - * Small util method to set the minimum levels of the nodes to zero. - * @private - */ - _setMinLevelToZero() { - let minLevel = 1e9; - // get the minimum level - for (let nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - if (this.hierarchicalLevels[nodeId] !== undefined) { - minLevel = Math.min(this.hierarchicalLevels[nodeId], minLevel); - } - } - } - - // subtract the minimum from the set so we have a range starting from 0 - for (let nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - if (this.hierarchicalLevels[nodeId] !== undefined) { - this.hierarchicalLevels[nodeId] -= minLevel; - } - } - } + this._crawlNetwork(levelByDirection); + this.hierarchical.setMinLevelToZero(this.body.nodes); } @@ -1175,21 +1286,13 @@ class LayoutEngine { */ _generateMap() { let fillInRelations = (parentNode, childNode) => { - if (this.hierarchicalLevels[childNode.id] > this.hierarchicalLevels[parentNode.id]) { - let parentNodeId = parentNode.id; - let childNodeId = childNode.id; - if (this.hierarchicalChildrenReference[parentNodeId] === undefined) { - this.hierarchicalChildrenReference[parentNodeId] = []; - } - this.hierarchicalChildrenReference[parentNodeId].push(childNodeId); - if (this.hierarchicalParentReference[childNodeId] === undefined) { - this.hierarchicalParentReference[childNodeId] = []; - } - this.hierarchicalParentReference[childNodeId].push(parentNodeId); + if (this.hierarchical.levels[childNode.id] > this.hierarchical.levels[parentNode.id]) { + this.hierarchical.addRelation(parentNode.id, childNode.id); } }; this._crawlNetwork(fillInRelations); + this.hierarchical.checkIfTree(); } @@ -1206,8 +1309,8 @@ class LayoutEngine { let crawler = (node, tree) => { if (progress[node.id] === undefined) { - if (this.hierarchicalTrees[node.id] === undefined) { - this.hierarchicalTrees[node.id] = tree; + if (this.hierarchical.trees[node.id] === undefined) { + this.hierarchical.trees[node.id] = tree; this.treeIndex = Math.max(tree, this.treeIndex); } @@ -1272,9 +1375,9 @@ class LayoutEngine { else { this.body.nodes[parentId].y += diff; } - if (this.hierarchicalChildrenReference[parentId] !== undefined) { - for (let i = 0; i < this.hierarchicalChildrenReference[parentId].length; i++) { - shifter(this.hierarchicalChildrenReference[parentId][i]); + if (this.hierarchical.childrenReference[parentId] !== undefined) { + for (let i = 0; i < this.hierarchical.childrenReference[parentId].length; i++) { + shifter(this.hierarchical.childrenReference[parentId][i]); } } }; @@ -1292,18 +1395,18 @@ class LayoutEngine { _findCommonParent(childA,childB) { let parents = {}; let iterateParents = (parents,child) => { - if (this.hierarchicalParentReference[child] !== undefined) { - for (let i = 0; i < this.hierarchicalParentReference[child].length; i++) { - let parent = this.hierarchicalParentReference[child][i]; + if (this.hierarchical.parentReference[child] !== undefined) { + for (let i = 0; i < this.hierarchical.parentReference[child].length; i++) { + let parent = this.hierarchical.parentReference[child][i]; parents[parent] = true; iterateParents(parents, parent) } } }; let findParent = (parents, child) => { - if (this.hierarchicalParentReference[child] !== undefined) { - for (let i = 0; i < this.hierarchicalParentReference[child].length; i++) { - let parent = this.hierarchicalParentReference[child][i]; + if (this.hierarchical.parentReference[child] !== undefined) { + for (let i = 0; i < this.hierarchical.parentReference[child].length; i++) { + let parent = this.hierarchical.parentReference[child][i]; if (parents[parent] !== undefined) { return {foundParent:parent, withChild:child}; } @@ -1350,6 +1453,18 @@ class LayoutEngine { } } + + /** + * Utility function to cut down on typing this all the time. + * + * TODO: use this in all applicable situations in this class. + * + * @private + */ + _isVertical() { + return (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU'); + } + /** * Abstract the getting of the position of a node so we do not have to repeat the direction check all the time. * @param node @@ -1384,9 +1499,6 @@ class LayoutEngine { } } } - - - } export default LayoutEngine; From 005fa974cbc65e3ff18ce75acbea67b01636bace Mon Sep 17 00:00:00 2001 From: wimrijnders Date: Sat, 13 May 2017 21:54:29 +0200 Subject: [PATCH 16/28] Fix #2952 Pre-render node images for interpolation (#3010) * CachedImage with preredendered zoom images; first working version. * Fixes for missing images and usage brokenImage. * Remove unused method * Added height member of CachedImage, note about svg's --- lib/network/CachedImage.js | 160 ++++++++++++++++++ lib/network/Images.js | 65 ++++--- .../components/nodes/util/CircleImageBase.js | 37 +--- 3 files changed, 204 insertions(+), 58 deletions(-) create mode 100644 lib/network/CachedImage.js diff --git a/lib/network/CachedImage.js b/lib/network/CachedImage.js new file mode 100644 index 00000000..6e027f08 --- /dev/null +++ b/lib/network/CachedImage.js @@ -0,0 +1,160 @@ + +/** + * Associates a canvas to a given image, containing a number of renderings + * of the image at various sizes. + * + * This technique is known as 'mipmapping'. + * + * NOTE: Images can also be of type 'data:svg+xml`. This code also works + * for svg, but the mipmapping may not be necessary. + */ +class CachedImage { + constructor(image) { + this.NUM_ITERATIONS = 4; // Number of items in the coordinates array + + this.image = new Image(); + this.canvas = document.createElement('canvas'); + } + + + /** + * Called when the image has been succesfully loaded. + */ + init() { + if (this.initialized()) return; + + var w = this.image.width; + var h = this.image.height; + + // Ease external access + this.width = w; + this.height = h; + + // Make canvas as small as possible + this.canvas.width = 3*w/4; + this.canvas.height = h/2; + + // Coordinates and sizes of images contained in the canvas + // Values per row: [top x, left y, width, height] + this.coordinates = [ + [ 0 , 0 , w/2 , h/2], + [ w/2 , 0 , w/4 , h/4], + [ w/2 , h/4, w/8 , h/8], + [ 5*w/8, h/4, w/16, h/16] + ]; + + this._fillMipMap(); + } + + + /** + * @return {Boolean} true if init() has been called, false otherwise. + */ + initialized() { + return (this.coordinates !== undefined); + } + + + /** + * Redraw main image in various sizes to the context. + * + * The rationale behind this is to reduce artefacts due to interpolation + * at differing zoom levels. + * + * Source: http://stackoverflow.com/q/18761404/1223531 + * + * This methods takes the resizing out of the drawing loop, in order to + * reduce performance overhead. + * + * @private + */ + _fillMipMap() { + var ctx = this.canvas.getContext('2d'); + + // First zoom-level comes from the image + var to = this.coordinates[0]; + ctx.drawImage(this.image, to[0], to[1], to[2], to[3]); + + // The rest are copy actions internal to the canvas/context + for (let iterations = 1; iterations < this.NUM_ITERATIONS; iterations++) { + let from = this.coordinates[iterations - 1]; + let to = this.coordinates[iterations]; + + ctx.drawImage(this.canvas, + from[0], from[1], from[2], from[3], + to[0], to[1], to[2], to[3] + ); + } + } + + + /** + * Draw the image, using the mipmap if necessary. + * + * MipMap is only used if param factor > 2; otherwise, original bitmap + * is resized. This is also used to skip mipmap usage, e.g. by setting factor = 1 + * + * Credits to 'Alex de Mulder' for original implementation. + * + * ctx {Context} context on which to draw zoomed image + * factor {Float} scale factor at which to draw + */ + drawImageAtPosition(ctx, factor, left, top, width, height) { + if (factor > 2 && this.initialized()) { + // Determine which zoomed image to use + factor *= 0.5; + let iterations = 0; + while (factor > 2 && iterations < this.NUM_ITERATIONS) { + factor *= 0.5; + iterations += 1; + } + + if (iterations >= this.NUM_ITERATIONS) { + iterations = this.NUM_ITERATIONS - 1; + } + //console.log("iterations: " + iterations); + + let from = this.coordinates[iterations]; + ctx.drawImage(this.canvas, + from[0], from[1], from[2], from[3], + left, top, width, height + ); + } else if (this._isImageOk()) { + // Draw image directly + ctx.drawImage(this.image, left, top, width, height); + } + } + + + /** + * Check if image is loaded + * + * Source: http://stackoverflow.com/a/1977898/1223531 + * + * @private + */ + _isImageOk(img) { + var img = this.image; + + // During the onload event, IE correctly identifies any images that + // weren’t downloaded as not complete. Others should too. Gecko-based + // browsers act like NS4 in that they report this incorrectly. + if (!img.complete) { + return false; + } + + // However, they do have two very useful properties: naturalWidth and + // naturalHeight. These give the true size of the image. If it failed + // to load, either of these should be zero. + + if (typeof img.naturalWidth !== "undefined" && img.naturalWidth === 0) { + return false; + } + + // No other way of checking: assume it’s ok. + return true; + } +} + + +export default CachedImage; diff --git a/lib/network/Images.js b/lib/network/Images.js index 464e4585..e9d5833f 100644 --- a/lib/network/Images.js +++ b/lib/network/Images.js @@ -1,29 +1,17 @@ +import CachedImage from './CachedImage'; + + /** * @class Images * This class loads images and keeps them stored. */ -class Images{ +class Images { constructor(callback){ this.images = {}; this.imageBroken = {}; this.callback = callback; } - /** - * @param {string} url The Url to cache the image as - * @return {Image} imageToLoadBrokenUrlOn The image object - */ - _addImageToCache (url, imageToCache) { - // IE11 fix -- thanks dponch! - if (imageToCache.width === 0) { - document.body.appendChild(imageToCache); - imageToCache.width = imageToCache.offsetWidth; - imageToCache.height = imageToCache.offsetHeight; - document.body.removeChild(imageToCache); - } - - this.images[url] = imageToCache; - } /** * @param {string} url The original Url that failed to load, if the broken image is successfully loaded it will be added to the cache using this Url as the key so that subsequent requests for this Url will return the broken image @@ -37,12 +25,11 @@ class Images{ //Clear the old subscription to the error event and put a new in place that only handle errors in loading the brokenImageUrl imageToLoadBrokenUrlOn.onerror = () => { console.error("Could not load brokenImage:", brokenUrl); - //Add an empty image to the cache so that when subsequent load calls are made for the url we don't try load the image and broken image again - this._addImageToCache(url, new Image()); + // cache item will contain empty image, this should be OK for default }; //Set the source of the image to the brokenUrl, this is actually what kicks off the loading of the broken image - imageToLoadBrokenUrlOn.src = brokenUrl; + imageToLoadBrokenUrlOn.image.src = brokenUrl; } /** @@ -65,28 +52,50 @@ class Images{ if (cachedImage) return cachedImage; //Create a new image - var img = new Image(); + var img = new CachedImage(); + + // Need to add to cache here, otherwise final return will spawn different copies of the same image, + // Also, there will be multiple loads of the same image. + this.images[url] = img; //Subscribe to the event that is raised if the image loads successfully - img.onload = () => { - //Add the image to the cache and then request a redraw - this._addImageToCache(url, img); + img.image.onload = () => { + // Properly init the cached item and then request a redraw + this._fixImageCoordinates(img.image); + img.init(); this._redrawWithImage(img); }; //Subscribe to the event that is raised if the image fails to load - img.onerror = () => { + img.image.onerror = () => { console.error("Could not load image:", url); //Try and load the image specified by the brokenUrl using this._tryloadBrokenUrl(url, brokenUrl, img); } - //Set the source of the image to the url, this is actuall what kicks off the loading of the image - img.src = url; + //Set the source of the image to the url, this is what actually kicks off the loading of the image + img.image.src = url; //Return the new image return img; - } + } + + + /** + * IE11 fix -- thanks dponch! + * + * Local helper function + * + * @private + */ + _fixImageCoordinates(imageToCache) { + if (imageToCache.width === 0) { + document.body.appendChild(imageToCache); + imageToCache.width = imageToCache.offsetWidth; + imageToCache.height = imageToCache.offsetHeight; + document.body.removeChild(imageToCache); + } + } } -export default Images; \ No newline at end of file +export default Images; diff --git a/lib/network/modules/components/nodes/util/CircleImageBase.js b/lib/network/modules/components/nodes/util/CircleImageBase.js index 99bff14a..86fd133e 100644 --- a/lib/network/modules/components/nodes/util/CircleImageBase.js +++ b/lib/network/modules/components/nodes/util/CircleImageBase.js @@ -1,4 +1,6 @@ -import NodeBase from '../util/NodeBase' +import NodeBase from './NodeBase'; +import CachedImage from '../../../../CachedImage'; + /** * NOTE: This is a bad base class @@ -122,37 +124,12 @@ class CircleImageBase extends NodeBase { // draw shadow if enabled this.enableShadow(ctx, values); - let factor = (this.imageObj.width / this.width) / this.body.view.scale; - if (factor > 2 && this.options.shapeProperties.interpolation === true) { - let w = this.imageObj.width; - let h = this.imageObj.height; - var can2 = document.createElement('canvas'); - can2.width = w; - can2.height = w; - var ctx2 = can2.getContext('2d'); - - factor *= 0.5; - w *= 0.5; - h *= 0.5; - ctx2.drawImage(this.imageObj, 0, 0, w, h); - - let distance = 0; - let iterations = 1; - while (factor > 2 && iterations < 4) { - ctx2.drawImage(can2, distance, 0, w, h, distance+w, 0, w/2, h/2); - distance += w; - factor *= 0.5; - w *= 0.5; - h *= 0.5; - iterations += 1; - } - ctx.drawImage(can2, distance, 0, w, h, this.left, this.top, this.width, this.height); - } - else { - // draw image - ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height); + let factor = 1; + if (this.options.shapeProperties.interpolation === true) { + factor = (this.imageObj.width / this.width) / this.body.view.scale; } + this.imageObj.drawImageAtPosition(ctx, factor, this.left, this.top, this.width, this.height); // disable shadows for other elements. this.disableShadow(ctx, values); From f67d16c96311df4c5807eebce6a98408a230019d Mon Sep 17 00:00:00 2001 From: wimrijnders Date: Sat, 13 May 2017 22:02:36 +0200 Subject: [PATCH 17/28] Fix #2922: bold label for selected ShapeBase classes. (#2924) --- lib/network/modules/components/nodes/shapes/Diamond.js | 4 ---- lib/network/modules/components/nodes/shapes/Dot.js | 4 ---- lib/network/modules/components/nodes/shapes/Square.js | 4 ---- lib/network/modules/components/nodes/shapes/Star.js | 4 ---- lib/network/modules/components/nodes/shapes/Triangle.js | 4 ---- lib/network/modules/components/nodes/shapes/TriangleDown.js | 4 ---- lib/network/modules/components/nodes/util/ShapeBase.js | 5 +++-- 7 files changed, 3 insertions(+), 26 deletions(-) diff --git a/lib/network/modules/components/nodes/shapes/Diamond.js b/lib/network/modules/components/nodes/shapes/Diamond.js index ecda7ea3..20a8f7db 100644 --- a/lib/network/modules/components/nodes/shapes/Diamond.js +++ b/lib/network/modules/components/nodes/shapes/Diamond.js @@ -7,10 +7,6 @@ class Diamond extends ShapeBase { super(options, body, labelModule) } - resize(ctx, selected = this.selected, hover = this.hover, values) { - this._resizeShape(selected, hover, values); - } - draw(ctx, x, y, selected, hover, values) { this._drawShape(ctx, 'diamond', 4, x, y, selected, hover, values); } diff --git a/lib/network/modules/components/nodes/shapes/Dot.js b/lib/network/modules/components/nodes/shapes/Dot.js index 6d054619..79746753 100644 --- a/lib/network/modules/components/nodes/shapes/Dot.js +++ b/lib/network/modules/components/nodes/shapes/Dot.js @@ -7,10 +7,6 @@ class Dot extends ShapeBase { super(options, body, labelModule) } - resize(ctx, selected = this.selected, hover = this.hover, values) { - this._resizeShape(selected, hover, values); - } - draw(ctx, x, y, selected, hover, values) { this._drawShape(ctx, 'circle', 2, x, y, selected, hover, values); } diff --git a/lib/network/modules/components/nodes/shapes/Square.js b/lib/network/modules/components/nodes/shapes/Square.js index b7b7e8bf..3dba0807 100644 --- a/lib/network/modules/components/nodes/shapes/Square.js +++ b/lib/network/modules/components/nodes/shapes/Square.js @@ -7,10 +7,6 @@ class Square extends ShapeBase { super(options, body, labelModule) } - resize() { - this._resizeShape(); - } - draw(ctx, x, y, selected, hover, values) { this._drawShape(ctx, 'square', 2, x, y, selected, hover, values); } diff --git a/lib/network/modules/components/nodes/shapes/Star.js b/lib/network/modules/components/nodes/shapes/Star.js index b99bc4e4..2b1437f8 100644 --- a/lib/network/modules/components/nodes/shapes/Star.js +++ b/lib/network/modules/components/nodes/shapes/Star.js @@ -7,10 +7,6 @@ class Star extends ShapeBase { super(options, body, labelModule) } - resize(ctx, selected, hover, values) { - this._resizeShape(selected, hover, values); - } - draw(ctx, x, y, selected, hover, values) { this._drawShape(ctx, 'star', 4, x, y, selected, hover, values); } diff --git a/lib/network/modules/components/nodes/shapes/Triangle.js b/lib/network/modules/components/nodes/shapes/Triangle.js index 2a75476c..9b35304f 100644 --- a/lib/network/modules/components/nodes/shapes/Triangle.js +++ b/lib/network/modules/components/nodes/shapes/Triangle.js @@ -7,10 +7,6 @@ class Triangle extends ShapeBase { super(options, body, labelModule) } - resize(ctx) { - this._resizeShape(); - } - draw(ctx, x, y, selected, hover, values) { this._drawShape(ctx, 'triangle', 3, x, y, selected, hover, values); } diff --git a/lib/network/modules/components/nodes/shapes/TriangleDown.js b/lib/network/modules/components/nodes/shapes/TriangleDown.js index a566a103..453056f3 100644 --- a/lib/network/modules/components/nodes/shapes/TriangleDown.js +++ b/lib/network/modules/components/nodes/shapes/TriangleDown.js @@ -7,10 +7,6 @@ class TriangleDown extends ShapeBase { super(options, body, labelModule) } - resize(ctx) { - this._resizeShape(); - } - draw(ctx, x, y, selected, hover, values) { this._drawShape(ctx, 'triangleDown', 3, x, y, selected, hover, values); } diff --git a/lib/network/modules/components/nodes/util/ShapeBase.js b/lib/network/modules/components/nodes/util/ShapeBase.js index 646e36b5..6c2e14e3 100644 --- a/lib/network/modules/components/nodes/util/ShapeBase.js +++ b/lib/network/modules/components/nodes/util/ShapeBase.js @@ -5,8 +5,9 @@ class ShapeBase extends NodeBase { super(options, body, labelModule) } - _resizeShape(selected = this.selected, hover = this.hover, values = { size: this.options.size }) { + resize(ctx, selected = this.selected, hover = this.hover, values = { size: this.options.size }) { if (this.needsRefresh(selected, hover)) { + this.labelModule.getTextSize(ctx, selected, hover); var size = 2 * values.size; this.width = size; this.height = size; @@ -15,7 +16,7 @@ class ShapeBase extends NodeBase { } _drawShape(ctx, shape, sizeMultiplier, x, y, selected, hover, values) { - this._resizeShape(selected, hover, values); + this.resize(ctx, selected, hover, values); this.left = x - this.width / 2; this.top = y - this.height / 2; From 63794a32d833015ee9aa27c351d9dc7904738800 Mon Sep 17 00:00:00 2001 From: Danny Larsen Date: Fri, 19 May 2017 11:20:39 +0200 Subject: [PATCH 18/28] mouseup and mousedown events fixes #3032 (#3059) * mouseup and mousedown events * Fix error --- docs/timeline/index.html | 27 +++++++++++++++++++ .../timeline/interaction/eventListeners.html | 8 ++++++ lib/timeline/Timeline.js | 24 ++++++++++++++--- lib/timeline/component/ItemSet.js | 2 +- 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/docs/timeline/index.html b/docs/timeline/index.html index 374b9f97..da2084f9 100644 --- a/docs/timeline/index.html +++ b/docs/timeline/index.html @@ -1584,6 +1584,33 @@ timeline.off('select', onSelect); + + mouseDown + + Passes a properties object as returned by the method Timeline.getEventProperties(event). + + Fired when the mouse down event is triggered over a timeline element. + + + + + mouseUp + + Passes a properties object as returned by the method Timeline.getEventProperties(event). + + Fired when the mouse up event is triggered over a timeline element. + + + + + mouseMove + + Passes a properties object as returned by the method Timeline.getEventProperties(event). + + Fired when the mouse is moved over a timeline element. + + + groupDragged diff --git a/examples/timeline/interaction/eventListeners.html b/examples/timeline/interaction/eventListeners.html index 91bacfb0..ae16dc67 100644 --- a/examples/timeline/interaction/eventListeners.html +++ b/examples/timeline/interaction/eventListeners.html @@ -72,6 +72,14 @@ logEvent('contextmenu', properties); }); + timeline.on('mouseDown', function (properties) { + logEvent('mouseDown', properties); + }); + + timeline.on('mouseUp', function (properties) { + logEvent('mouseUp', properties); + }); + // other possible events: // timeline.on('mouseOver', function (properties) { diff --git a/lib/timeline/Timeline.js b/lib/timeline/Timeline.js index 02d633a4..8b870578 100644 --- a/lib/timeline/Timeline.js +++ b/lib/timeline/Timeline.js @@ -139,9 +139,27 @@ function Timeline (container, items, groups, options) { this.dom.root.onmouseover = function (event) { me.emit('mouseOver', me.getEventProperties(event)) }; - this.dom.root.onmousemove = function (event) { - me.emit('mouseMove', me.getEventProperties(event)) - }; + if(window.PointerEvent) { + this.dom.root.onpointerdown = function (event) { + me.emit('mouseDown', me.getEventProperties(event)) + }; + this.dom.root.onpointermove = function (event) { + me.emit('mouseMove', me.getEventProperties(event)) + }; + this.dom.root.onpointerup = function (event) { + me.emit('mouseUp', me.getEventProperties(event)) + }; + } else { + this.dom.root.onmousemove = function (event) { + me.emit('mouseMove', me.getEventProperties(event)) + }; + this.dom.root.onmousedown = function (event) { + me.emit('mouseDown', me.getEventProperties(event)) + }; + this.dom.root.onmouseup = function (event) { + me.emit('mouseUp', me.getEventProperties(event)) + }; + } //Single time autoscale/fit this.fitDone = false; diff --git a/lib/timeline/component/ItemSet.js b/lib/timeline/component/ItemSet.js index 002f294a..09f16f39 100644 --- a/lib/timeline/component/ItemSet.js +++ b/lib/timeline/component/ItemSet.js @@ -2220,7 +2220,7 @@ ItemSet.prototype.groupFromTarget = function(event) { var clientY = event.center ? event.center.y : event.clientY; var groupIds = this.groupIds; - if (groupIds.length <= 0) { + if (groupIds.length <= 0 && this.groupsData) { groupIds = this.groupsData.getIds({ order: this.options.groupOrder }); From 02911c0d3a56a7644fe5089451fdf73ebb47ef5b Mon Sep 17 00:00:00 2001 From: wimrijnders Date: Fri, 19 May 2017 11:32:47 +0200 Subject: [PATCH 19/28] Add command line options to mocha for running tests (#3064) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2695c51f..a9ae4053 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ ], "main": "./dist/vis.js", "scripts": { - "test": "mocha", + "test": "mocha --compilers js:babel-core/register", "build": "gulp", "lint": "eslint lib", "watch": "gulp watch", From bf80a902f27be128ef4033431088a3d0cf7138ef Mon Sep 17 00:00:00 2001 From: wimrijnders Date: Fri, 19 May 2017 11:37:24 +0200 Subject: [PATCH 20/28] Avoid overriding standard context method ellipse() (#3072) --- lib/network/modules/components/nodes/shapes/Ellipse.js | 2 +- lib/network/shapes.js | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/network/modules/components/nodes/shapes/Ellipse.js b/lib/network/modules/components/nodes/shapes/Ellipse.js index f10f0a40..d993efe3 100644 --- a/lib/network/modules/components/nodes/shapes/Ellipse.js +++ b/lib/network/modules/components/nodes/shapes/Ellipse.js @@ -28,7 +28,7 @@ class Ellipse extends NodeBase { ctx.strokeStyle = values.borderColor; ctx.fillStyle = values.color; - ctx.ellipse(this.left, this.top, this.width, this.height); + ctx.ellipse_vis(this.left, this.top, this.width, this.height); // draw shadow if enabled this.enableShadow(ctx, values); diff --git a/lib/network/shapes.js b/lib/network/shapes.js index 9346f482..270b2913 100644 --- a/lib/network/shapes.js +++ b/lib/network/shapes.js @@ -149,8 +149,10 @@ if (typeof CanvasRenderingContext2D !== 'undefined') { /** * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas + * + * Postfix '_vis' added to discern it from standard method ellipse(). */ - CanvasRenderingContext2D.prototype.ellipse = function (x, y, w, h) { + CanvasRenderingContext2D.prototype.ellipse_vis = function (x, y, w, h) { var kappa = .5522848, ox = (w / 2) * kappa, // control point offset horizontal oy = (h / 2) * kappa, // control point offset vertical From 168e79d29950bd008dbb52e2637ee32e8d2a8151 Mon Sep 17 00:00:00 2001 From: wimrijnders Date: Fri, 19 May 2017 11:46:16 +0200 Subject: [PATCH 21/28] Use get() to get data from DataSet/View instead of directly accessing member _data. (#3069) --- lib/network/modules/NodesHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/network/modules/NodesHandler.js b/lib/network/modules/NodesHandler.js index 7c56f3d2..f6f26241 100644 --- a/lib/network/modules/NodesHandler.js +++ b/lib/network/modules/NodesHandler.js @@ -330,7 +330,7 @@ class NodesHandler { if (nodes.hasOwnProperty(nodeId)) { node = nodes[nodeId]; } - let data = this.body.data.nodes._data[nodeId]; + let data = this.body.data.nodes.get(nodeId); if (node !== undefined && data !== undefined) { if (clearPositions === true) { node.setOptions({x:null, y:null}); From 000fe8222bcbc6728a0f206b0546cfe3ad80bae8 Mon Sep 17 00:00:00 2001 From: wimrijnders Date: Fri, 19 May 2017 11:54:33 +0200 Subject: [PATCH 22/28] Added check on mission var 'options', refactoring. (#3055) --- .../modules/components/shared/Label.js | 84 ++++++++++++------- 1 file changed, 55 insertions(+), 29 deletions(-) diff --git a/lib/network/modules/components/shared/Label.js b/lib/network/modules/components/shared/Label.js index becbf0d3..ea787b89 100644 --- a/lib/network/modules/components/shared/Label.js +++ b/lib/network/modules/components/shared/Label.js @@ -37,11 +37,7 @@ class Label { } static parseOptions(parentOptions, newOptions, allowDeletion = false) { - if (typeof newOptions.font === 'string') { - let newOptionsArray = newOptions.font.split(" "); - parentOptions.size = newOptionsArray[0].replace("px",''); - parentOptions.face = newOptionsArray[1]; - parentOptions.color = newOptionsArray[2]; + if (Label.parseFontString(parentOptions, newOptions.font)) { parentOptions.vadjust = 0; } else if (typeof newOptions.font === 'object') { @@ -51,6 +47,32 @@ class Label { parentOptions.vadjust = Number(parentOptions.vadjust); } + + /** + * If in-variable is a string, parse it as a font specifier. + * + * Note that following is not done here and have to be done after the call: + * - No number conversion (size) + * - Not all font options are set (vadjust, mod) + * + * @param inOptions {Object} font options to parse + * @param outOptions {Object} out-parameter, object in which to store the parse results (if any) + * + * @return true if font parsed as string, false otherwise + */ + static parseFontString(outOptions, inOptions) { + if (!inOptions || typeof inOptions !== 'string') return false; + + let newOptionsArray = inOptions.split(" "); + + outOptions.size = newOptionsArray[0].replace("px",''); + outOptions.face = newOptionsArray[1]; + outOptions.color = newOptionsArray[2]; + + return true; + } + + // set the width and height constraints based on 'nearest' value constrain(elementOptions, options, defaultOptions) { this.fontOptions.constrainWidth = false; @@ -141,13 +163,17 @@ class Label { if (!this.fontOptions.multi) return; /** - * Get property value from options.font[mod][property] if present. - * If mod not passed, use property value from options.font[property]. + * Resolve the font options path. + * If valid, return a reference to the object in question. + * Otherwise, just return null. * - * @return value if found, null otherwise. + * param 'mod' is optional. + * + * options {Object} base object to determine path from + * mod {string|undefined} if present, sub path for the mod-font */ - var getP = function(options, mod, property) { - if (!options.font) return null; + var pathP = function(options, mod) { + if (!options || !options.font) return null; var opt = options.font; @@ -156,7 +182,20 @@ class Label { opt = opt[mod]; } - if (opt.hasOwnProperty(property)) { + return opt; + }; + + + /** + * Get property value from options.font[mod][property] if present. + * If mod not passed, use property value from options.font[property]. + * + * @return value if found, null otherwise. + */ + var getP = function(options, mod, property) { + let opt = pathP(options, mod); + + if (opt && opt.hasOwnProperty(property)) { return opt[property]; } @@ -169,16 +208,7 @@ class Label { let modOptions = this.fontOptions[mod]; let modDefaults = defaultOptions.font[mod]; - let optionsFontMod; - if (options.font) { - optionsFontMod = options.font[mod]; - } - - if (typeof optionsFontMod === 'string') { - let modOptionsArray = optionsFontMod.split(" "); - modOptions.size = modOptionsArray[0].replace("px",""); - modOptions.face = modOptionsArray[1]; - modOptions.color = modOptionsArray[2]; + if (Label.parseFontString(modOptions, pathP(options, mod))) { modOptions.vadjust = this.fontOptions.vadjust; modOptions.mod = modDefaults.mod; } else { @@ -187,10 +217,8 @@ class Label { // much 'natural' versatility as we can get, so a simple global // change propagates in an expected way, even if not stictly logical. - // face: We want to capture any direct settings and overrides, but - // fall back to the base font if there aren't any. We make a - // special exception for mono, since we probably don't want to - // sync to a the base font face. + // 'face' has a special exception for mono, since we probably + // don't want to sync to the base font face. modOptions.face = getP(options , mod, 'face') || getP(groupOptions, mod, 'face') || @@ -199,7 +227,7 @@ class Label { this.fontOptions.face ; - // color: this is handled just like the face, except for specific default for 'mono' + // 'color' follows the standard flow modOptions.color = getP(options , mod, 'color') || getP(groupOptions, mod, 'color') || @@ -207,9 +235,7 @@ class Label { this.fontOptions.color ; - // mod: this is handled just like the face, except we never grab the - // base font's mod. We know they're in the defaultOptions, and unless - // we've been steered away from them, we use the default. + // 'mode' follows the standard flow modOptions.mod = getP(options , mod, 'mod') || getP(groupOptions, mod, 'mod') || From 85e36ed575ada274de7ce6b17b12ef1ce85e262b Mon Sep 17 00:00:00 2001 From: wimrijnders Date: Sat, 20 May 2017 08:34:33 +0200 Subject: [PATCH 23/28] Configurable minimum and maximum sizes for dot-size graphs (#2849) Sorry for the delay on this... It took me a while to confirm this PR again. I don;t know why I didn't merge it at the time I reviewed it. Looks great to me! --- docs/graph3d/index.html | 18 +++++++++++++++++ examples/graph3d/08_dot_cloud_size.html | 4 +++- lib/graph3d/Graph3d.js | 26 ++++++++++++++++--------- lib/graph3d/Settings.js | 2 ++ 4 files changed, 40 insertions(+), 10 deletions(-) diff --git a/docs/graph3d/index.html b/docs/graph3d/index.html index 85829ad1..69d28ad6 100644 --- a/docs/graph3d/index.html +++ b/docs/graph3d/index.html @@ -386,6 +386,24 @@ var options = { Ratio of the size of the dots with respect to the width of the graph. + + dotSizeMinFraction + number + 0.5 + Size of minimum-value dot as a fraction of dotSizeRatio. + Applicable when using style dot-size. + + + + + dotSizeMaxFraction + number + 2.5 + Size of maximum-value dot as a fraction of dotSizeRatio. + Applicable when using style dot-size. + + + gridColor string diff --git a/examples/graph3d/08_dot_cloud_size.html b/examples/graph3d/08_dot_cloud_size.html index 1d6fb47f..99ab8ffd 100644 --- a/examples/graph3d/08_dot_cloud_size.html +++ b/examples/graph3d/08_dot_cloud_size.html @@ -49,7 +49,9 @@ horizontal: -0.54, vertical: 0.5, distance: 1.6 - } + }, + dotSizeMinFraction: 0.5, + dotSizeMaxFraction: 2.5 }; // create our graph diff --git a/lib/graph3d/Graph3d.js b/lib/graph3d/Graph3d.js index e24ed2e4..29371ff7 100755 --- a/lib/graph3d/Graph3d.js +++ b/lib/graph3d/Graph3d.js @@ -53,7 +53,10 @@ var DEFAULTS = { showShadow : false, keepAspectRatio : true, 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 + + dotSizeRatio : 0.02, // size of the dots as a fraction of the graph width + dotSizeMinFraction: 0.5, // size of min-value dot as a fraction of dotSizeRatio + dotSizeMaxFraction: 2.5, // size of max-value dot as a fraction of dotSizeRatio showAnimationControls: autoByDefault, animationInterval : 1000, // milliseconds @@ -1014,7 +1017,8 @@ Graph3d.prototype._getLegendWidth = function() { if (this.style === Graph3d.STYLE.DOTSIZE) { var dotSize = this._dotSize(); - width = dotSize / 2 + dotSize * 2; + //width = dotSize / 2 + dotSize * 2; + width = dotSize * this.dotSizeMaxFraction; } else if (this.style === Graph3d.STYLE.BARSIZE) { width = this.xBarWidth ; } else { @@ -1086,8 +1090,8 @@ Graph3d.prototype._redrawLegend = function() { // draw the size legend box var widthMin; if (this.style === Graph3d.STYLE.DOTSIZE) { - var dotSize = this._dotSize(); - widthMin = dotSize / 2; // px + // Get the proportion to max and min right + widthMin = width * (this.dotSizeMinFraction / this.dotSizeMaxFraction); } else if (this.style === Graph3d.STYLE.BARSIZE) { //widthMin = this.xBarWidth * 0.2 this is wrong - barwidth measures in terms of xvalues } @@ -1096,7 +1100,7 @@ Graph3d.prototype._redrawLegend = function() { ctx.beginPath(); ctx.moveTo(left, top); ctx.lineTo(right, top); - ctx.lineTo(right - width + widthMin, bottom); + ctx.lineTo(left + widthMin, bottom); ctx.lineTo(left, bottom); ctx.closePath(); ctx.fill(); @@ -1821,10 +1825,14 @@ Graph3d.prototype._redrawDotColorGraphPoint = function(ctx, point) { * Draw single datapoint for graph style 'dot-size'. */ Graph3d.prototype._redrawDotSizeGraphPoint = function(ctx, point) { - var dotSize = this._dotSize(); - var fraction = (point.point.value - this.valueRange.min) / this.valueRange.range(); - var size = dotSize/2 + 2*dotSize * fraction; - var colors = this._getColorsSize(); + var dotSize = this._dotSize(); + var fraction = (point.point.value - this.valueRange.min) / this.valueRange.range(); + + var sizeMin = dotSize*this.dotSizeMinFraction; + var sizeRange = dotSize*this.dotSizeMaxFraction - sizeMin; + var size = sizeMin + sizeRange*fraction; + + var colors = this._getColorsSize(); this._drawCircle(ctx, point, colors.fill, colors.border, size); }; diff --git a/lib/graph3d/Settings.js b/lib/graph3d/Settings.js index 484a40b0..b49ef7f3 100755 --- a/lib/graph3d/Settings.js +++ b/lib/graph3d/Settings.js @@ -63,6 +63,8 @@ var OPTIONKEYS = [ 'keepAspectRatio', 'verticalRatio', 'dotSizeRatio', + 'dotSizeMinFraction', + 'dotSizeMaxFraction', 'showAnimationControls', 'animationInterval', 'animationPreload', From 9a564417d03742a161f20f96fdff004ca8ca714a Mon Sep 17 00:00:00 2001 From: wimrijnders Date: Sat, 20 May 2017 08:40:22 +0200 Subject: [PATCH 24/28] Fixes for loading images into image nodes (#2964) * Add warnings and errors for image loading * Fixes and commenting for image loading * Fix typo * Small fixes due to review --- lib/network/Images.js | 8 ++- lib/network/modules/components/Node.js | 56 ++++++++++++++----- .../components/nodes/shapes/CircularImage.js | 10 +--- .../modules/components/nodes/shapes/Image.js | 10 +--- .../components/nodes/util/CircleImageBase.js | 32 +++++++++-- 5 files changed, 77 insertions(+), 39 deletions(-) diff --git a/lib/network/Images.js b/lib/network/Images.js index e9d5833f..7256f2a8 100644 --- a/lib/network/Images.js +++ b/lib/network/Images.js @@ -19,8 +19,12 @@ class Images { * @return {Image} imageToLoadBrokenUrlOn The image object */ _tryloadBrokenUrl (url, brokenUrl, imageToLoadBrokenUrlOn) { - //If any of the parameters aren't specified then exit the function because nothing constructive can be done - if (url === undefined || brokenUrl === undefined || imageToLoadBrokenUrlOn === undefined) return; + //If these parameters aren't specified then exit the function because nothing constructive can be done + if (url === undefined || imageToLoadBrokenUrlOn === undefined) return; + if (brokenUrl === undefined) { + console.warn("No broken url image defined"); + return; + } //Clear the old subscription to the error event and put a new in place that only handle errors in loading the brokenImageUrl imageToLoadBrokenUrlOn.onerror = () => { diff --git a/lib/network/modules/components/Node.js b/lib/network/modules/components/Node.js index be4000ff..28f2347e 100644 --- a/lib/network/modules/components/Node.js +++ b/lib/network/modules/components/Node.js @@ -141,21 +141,7 @@ class Node { this.choosify(options); - // load the images - if (this.options.image !== undefined) { - if (this.imagelist) { - if (typeof this.options.image === 'string') { - this.imageObj = this.imagelist.load(this.options.image, this.options.brokenImage, this.id); - } else { - this.imageObj = this.imagelist.load(this.options.image.unselected, this.options.brokenImage, this.id); - this.imageObjAlt = this.imagelist.load(this.options.image.selected, this.options.brokenImage, this.id); - } - } - else { - throw "No imagelist provided"; - } - } - + this._load_images(); this.updateLabelModule(options); this.updateShape(currentShape); this.labelModule.propagateFonts(this.nodeOptions, options, this.defaultOptions); @@ -167,6 +153,46 @@ class Node { } + /** + * Load the images from the options, for the nodes that need them. + * + * TODO: The imageObj members should be moved to CircularImageBase. + * It's the only place where they are required. + * + * @private + */ + _load_images() { + // Don't bother loading for nodes without images + if (this.options.shape !== 'circularImage' && this.options.shape !== 'image') { + return; + } + + if (this.options.image === undefined) { + throw "Option image must be defined for node type '" + this.options.shape + "'"; + } + + if (this.imagelist === undefined) { + throw "Internal Error: No images provided"; + } + + if (typeof this.options.image === 'string') { + this.imageObj = this.imagelist.load(this.options.image, this.options.brokenImage, this.id); + } else { + if (this.options.image.unselected === undefined) { + throw "No unselected image provided"; + } + + this.imageObj = this.imagelist.load(this.options.image.unselected, this.options.brokenImage, this.id); + + if (this.options.image.selected !== undefined) { + this.imageObjAlt = this.imagelist.load(this.options.image.selected, this.options.brokenImage, this.id); + } else { + this.imageObjAlt = undefined; + } + } + } + + /** * This process all possible shorthands in the new options and makes sure that the parentOptions are fully defined. * Static so it can also be used by the handler. diff --git a/lib/network/modules/components/nodes/shapes/CircularImage.js b/lib/network/modules/components/nodes/shapes/CircularImage.js index 95c0b5ba..6f743d5b 100644 --- a/lib/network/modules/components/nodes/shapes/CircularImage.js +++ b/lib/network/modules/components/nodes/shapes/CircularImage.js @@ -29,14 +29,8 @@ class CircularImage extends CircleImageBase { } draw(ctx, x, y, selected, hover, values) { - // switch images depending on 'selected' if imageObjAlt exists - if (this.imageObjAlt) { - this.switchImages(selected); - } - - this.selected = selected; - - this.resize(ctx, selected, hover); + this.switchImages(selected); + this.resize(); this.left = x - this.width / 2; this.top = y - this.height / 2; diff --git a/lib/network/modules/components/nodes/shapes/Image.js b/lib/network/modules/components/nodes/shapes/Image.js index a95e5cd4..ab1e7135 100644 --- a/lib/network/modules/components/nodes/shapes/Image.js +++ b/lib/network/modules/components/nodes/shapes/Image.js @@ -16,14 +16,8 @@ class Image extends CircleImageBase { } draw(ctx, x, y, selected, hover, values) { - // switch images depending on 'selected' if imageObjAlt exists - if (this.imageObjAlt) { - this.switchImages(selected); - } - - this.selected = selected; - - this.resize(ctx, selected, hover); + this.switchImages(selected); + this.resize(); this.left = x - this.width / 2; this.top = y - this.height / 2; diff --git a/lib/network/modules/components/nodes/util/CircleImageBase.js b/lib/network/modules/components/nodes/util/CircleImageBase.js index 86fd133e..4209260e 100644 --- a/lib/network/modules/components/nodes/util/CircleImageBase.js +++ b/lib/network/modules/components/nodes/util/CircleImageBase.js @@ -24,9 +24,25 @@ class CircleImageBase extends NodeBase { setOptions(options, imageObj, imageObjAlt) { this.options = options; - this.setImages(imageObj, imageObjAlt); + + if (!(imageObj === undefined && imageObjAlt === undefined)) { + this.setImages(imageObj, imageObjAlt); + } } + + /** + * Set the images for this node. + * + * The images can be updated after the initial setting of options; + * therefore, this method needs to be reentrant. + * + * For correct working in error cases, it is necessary to properly set + * field 'nodes.brokenImage' in the options. + * + * @param {Image} imageObj required; main image to show for this node + * @param {Image|undefined} optional; image to show when node is selected + */ setImages(imageObj, imageObjAlt) { if (imageObjAlt && this.selected) { this.imageObj = imageObjAlt; @@ -38,17 +54,21 @@ class CircleImageBase extends NodeBase { } /** - * Switch between the base and the selected image. + * Set selection and switch between the base and the selected image. + * + * Do the switch only if imageObjAlt exists. + * + * @param {true|false} selected value of new selected state for current node */ switchImages(selected) { - if ((selected && !this.selected) || (!selected && this.selected)) { + var selection_changed = ((selected && !this.selected) || (!selected && this.selected)); + this.selected = selected; // Remember new selection + + if (this.imageObjAlt !== undefined && selection_changed) { let imageTmp = this.imageObj; this.imageObj = this.imageObjAlt; this.imageObjAlt = imageTmp; } - - // keep current state in memory - this.selected = selected; } /** From e25db74b4b86da7830d8c65985ef4675793a5259 Mon Sep 17 00:00:00 2001 From: Pat Sissons Date: Sat, 20 May 2017 02:17:27 -0700 Subject: [PATCH 25/28] replacing all ES6 imports with CJS require calls (#3063) * replacing all ES6 imports with CJS require calls resolves #2934 used the following regex to apply the changes in lib: s/import\s+(\w+)\s+from\s+(.*);\s*$/var $1 = require($2).default;/ s/import\s+(\w+)\s+from\s+(.*)\s*$/var $1 = require($2).default;/ * cleaning up inconsistencies --- lib/network/Network.js | 36 +++++++++++------------ lib/network/modules/Clustering.js | 4 +-- lib/network/modules/EdgesHandler.js | 4 +-- lib/network/modules/InteractionHandler.js | 4 +-- lib/network/modules/LayoutEngine.js | 2 +- lib/network/modules/NodesHandler.js | 4 +-- lib/network/modules/PhysicsEngine.js | 16 +++++----- lib/network/modules/SelectionHandler.js | 4 +-- lib/network/modules/View.js | 2 +- lib/network/modules/components/Edge.js | 10 +++---- lib/network/modules/components/Node.js | 36 +++++++++++------------ lib/shared/Configurator.js | 2 +- lib/timeline/Graph2d.js | 4 +-- lib/timeline/Timeline.js | 4 +-- lib/timeline/component/ItemSet.js | 2 +- 15 files changed, 67 insertions(+), 67 deletions(-) diff --git a/lib/network/Network.js b/lib/network/Network.js index 884c3352..4d194f5f 100644 --- a/lib/network/Network.js +++ b/lib/network/Network.js @@ -10,24 +10,24 @@ let gephiParser = require('./gephiParser'); let Activator = require('../shared/Activator'); let locales = require('./locales'); -import Images from './Images'; -import Groups from './modules/Groups'; -import NodesHandler from './modules/NodesHandler'; -import EdgesHandler from './modules/EdgesHandler'; -import PhysicsEngine from './modules/PhysicsEngine'; -import ClusterEngine from './modules/Clustering'; -import CanvasRenderer from './modules/CanvasRenderer'; -import Canvas from './modules/Canvas'; -import View from './modules/View'; -import InteractionHandler from './modules/InteractionHandler'; -import SelectionHandler from "./modules/SelectionHandler"; -import LayoutEngine from "./modules/LayoutEngine"; -import ManipulationSystem from "./modules/ManipulationSystem"; -import Configurator from "./../shared/Configurator"; -import Validator from "./../shared/Validator"; -import {printStyle} from "./../shared/Validator"; -import {allOptions, configureOptions} from './options.js'; -import KamadaKawai from "./modules/KamadaKawai.js" +var Images = require('./Images').default; +var Groups = require('./modules/Groups').default; +var NodesHandler = require('./modules/NodesHandler').default; +var EdgesHandler = require('./modules/EdgesHandler').default; +var PhysicsEngine = require('./modules/PhysicsEngine').default; +var ClusterEngine = require('./modules/Clustering').default; +var CanvasRenderer = require('./modules/CanvasRenderer').default; +var Canvas = require('./modules/Canvas').default; +var View = require('./modules/View').default; +var InteractionHandler = require('./modules/InteractionHandler').default; +var SelectionHandler = require("./modules/SelectionHandler").default; +var LayoutEngine = require("./modules/LayoutEngine").default; +var ManipulationSystem = require("./modules/ManipulationSystem").default; +var Configurator = require("./../shared/Configurator").default; +var Validator = require("./../shared/Validator").default; +var {printStyle} = require('./../shared/Validator'); +var {allOptions, configureOptions} = require('./options.js'); +var KamadaKawai = require("./modules/KamadaKawai.js").default; /** diff --git a/lib/network/modules/Clustering.js b/lib/network/modules/Clustering.js index 3de5198e..b039a0fc 100644 --- a/lib/network/modules/Clustering.js +++ b/lib/network/modules/Clustering.js @@ -1,6 +1,6 @@ let util = require("../../util"); -import NetworkUtil from '../NetworkUtil'; -import Cluster from './components/nodes/Cluster' +var NetworkUtil = require('../NetworkUtil').default; +var Cluster = require('./components/nodes/Cluster').default; class ClusterEngine { constructor(body) { diff --git a/lib/network/modules/EdgesHandler.js b/lib/network/modules/EdgesHandler.js index f5afc13b..d62ba685 100644 --- a/lib/network/modules/EdgesHandler.js +++ b/lib/network/modules/EdgesHandler.js @@ -2,8 +2,8 @@ var util = require("../../util"); var DataSet = require('../../DataSet'); var DataView = require('../../DataView'); -import Edge from "./components/Edge" -import Label from "./components/shared/Label" +var Edge = require("./components/Edge").default; +var Label = require("./components/shared/Label").default; class EdgesHandler { constructor(body, images, groups) { diff --git a/lib/network/modules/InteractionHandler.js b/lib/network/modules/InteractionHandler.js index 3f7a139d..c0b8dade 100644 --- a/lib/network/modules/InteractionHandler.js +++ b/lib/network/modules/InteractionHandler.js @@ -1,7 +1,7 @@ let util = require('../../util'); -import NavigationHandler from './components/NavigationHandler' -import Popup from './../../shared/Popup' +var NavigationHandler = require('./components/NavigationHandler').default; +var Popup = require('./../../shared/Popup').default; class InteractionHandler { constructor(body, canvas, selectionHandler) { diff --git a/lib/network/modules/LayoutEngine.js b/lib/network/modules/LayoutEngine.js index e2466792..57b9e092 100644 --- a/lib/network/modules/LayoutEngine.js +++ b/lib/network/modules/LayoutEngine.js @@ -1,7 +1,7 @@ 'use strict'; let util = require('../../util'); -import NetworkUtil from '../NetworkUtil'; +var NetworkUtil = require('../NetworkUtil').default; /** diff --git a/lib/network/modules/NodesHandler.js b/lib/network/modules/NodesHandler.js index f6f26241..f9999327 100644 --- a/lib/network/modules/NodesHandler.js +++ b/lib/network/modules/NodesHandler.js @@ -2,8 +2,8 @@ let util = require("../../util"); let DataSet = require('../../DataSet'); let DataView = require('../../DataView'); -import Node from "./components/Node"; -import Label from "./components/shared/Label"; +var Node = require("./components/Node").default; +var Label = require("./components/shared/Label").default; class NodesHandler { constructor(body, images, groups, layoutEngine) { diff --git a/lib/network/modules/PhysicsEngine.js b/lib/network/modules/PhysicsEngine.js index 3555c2ff..742e13d3 100644 --- a/lib/network/modules/PhysicsEngine.js +++ b/lib/network/modules/PhysicsEngine.js @@ -1,11 +1,11 @@ -import BarnesHutSolver from './components/physics/BarnesHutSolver'; -import Repulsion from './components/physics/RepulsionSolver'; -import HierarchicalRepulsion from './components/physics/HierarchicalRepulsionSolver'; -import SpringSolver from './components/physics/SpringSolver'; -import HierarchicalSpringSolver from './components/physics/HierarchicalSpringSolver'; -import CentralGravitySolver from './components/physics/CentralGravitySolver'; -import ForceAtlas2BasedRepulsionSolver from './components/physics/FA2BasedRepulsionSolver'; -import ForceAtlas2BasedCentralGravitySolver from './components/physics/FA2BasedCentralGravitySolver'; +var BarnesHutSolver = require('./components/physics/BarnesHutSolver').default; +var Repulsion = require('./components/physics/RepulsionSolver').default; +var HierarchicalRepulsion = require('./components/physics/HierarchicalRepulsionSolver').default; +var SpringSolver = require('./components/physics/SpringSolver').default; +var HierarchicalSpringSolver = require('./components/physics/HierarchicalSpringSolver').default; +var CentralGravitySolver = require('./components/physics/CentralGravitySolver').default; +var ForceAtlas2BasedRepulsionSolver = require('./components/physics/FA2BasedRepulsionSolver').default; +var ForceAtlas2BasedCentralGravitySolver = require('./components/physics/FA2BasedCentralGravitySolver').default; var util = require('../../util'); diff --git a/lib/network/modules/SelectionHandler.js b/lib/network/modules/SelectionHandler.js index 101b9f4a..56768e29 100644 --- a/lib/network/modules/SelectionHandler.js +++ b/lib/network/modules/SelectionHandler.js @@ -1,5 +1,5 @@ -import Node from './components/Node'; -import Edge from './components/Edge'; +var Node = require('./components/Node').default; +var Edge = require('./components/Edge').default; let util = require('../../util'); diff --git a/lib/network/modules/View.js b/lib/network/modules/View.js index d1004c80..07029783 100644 --- a/lib/network/modules/View.js +++ b/lib/network/modules/View.js @@ -1,6 +1,6 @@ let util = require('../../util'); -import NetworkUtil from '../NetworkUtil'; +var NetworkUtil = require('../NetworkUtil').default; class View { constructor(body, canvas) { diff --git a/lib/network/modules/components/Edge.js b/lib/network/modules/components/Edge.js index f84a9189..8f04b8c8 100644 --- a/lib/network/modules/components/Edge.js +++ b/lib/network/modules/components/Edge.js @@ -1,10 +1,10 @@ var util = require('../../../util'); -import Label from './shared/Label' -import CubicBezierEdge from './edges/CubicBezierEdge' -import BezierEdgeDynamic from './edges/BezierEdgeDynamic' -import BezierEdgeStatic from './edges/BezierEdgeStatic' -import StraightEdge from './edges/StraightEdge' +var Label = require('./shared/Label').default; +var CubicBezierEdge = require('./edges/CubicBezierEdge').default; +var BezierEdgeDynamic = require('./edges/BezierEdgeDynamic').default; +var BezierEdgeStatic = require('./edges/BezierEdgeStatic').default; +var StraightEdge = require('./edges/StraightEdge').default; /** * @class Edge diff --git a/lib/network/modules/components/Node.js b/lib/network/modules/components/Node.js index 28f2347e..296815d9 100644 --- a/lib/network/modules/components/Node.js +++ b/lib/network/modules/components/Node.js @@ -1,23 +1,23 @@ var util = require('../../../util'); -import Label from './shared/Label' - -import Box from './nodes/shapes/Box' -import Circle from './nodes/shapes/Circle' -import CircularImage from './nodes/shapes/CircularImage' -import Database from './nodes/shapes/Database' -import Diamond from './nodes/shapes/Diamond' -import Dot from './nodes/shapes/Dot' -import Ellipse from './nodes/shapes/Ellipse' -import Icon from './nodes/shapes/Icon' -import Image from './nodes/shapes/Image' -import Square from './nodes/shapes/Square' -import Star from './nodes/shapes/Star' -import Text from './nodes/shapes/Text' -import Triangle from './nodes/shapes/Triangle' -import TriangleDown from './nodes/shapes/TriangleDown' -import Validator from "../../../shared/Validator"; -import {printStyle} from "../../../shared/Validator"; +var Label = require('./shared/Label').default; + +var Box = require('./nodes/shapes/Box').default; +var Circle = require('./nodes/shapes/Circle').default; +var CircularImage = require('./nodes/shapes/CircularImage').default; +var Database = require('./nodes/shapes/Database').default; +var Diamond = require('./nodes/shapes/Diamond').default; +var Dot = require('./nodes/shapes/Dot').default; +var Ellipse = require('./nodes/shapes/Ellipse').default; +var Icon = require('./nodes/shapes/Icon').default; +var Image = require('./nodes/shapes/Image').default; +var Square = require('./nodes/shapes/Square').default; +var Star = require('./nodes/shapes/Star').default; +var Text = require('./nodes/shapes/Text').default; +var Triangle = require('./nodes/shapes/Triangle').default; +var TriangleDown = require('./nodes/shapes/TriangleDown').default; +var Validator = require("../../../shared/Validator").default; +var { printStyle } = require("../../../shared/Validator"); /** diff --git a/lib/shared/Configurator.js b/lib/shared/Configurator.js index e564bc2d..3e70decc 100644 --- a/lib/shared/Configurator.js +++ b/lib/shared/Configurator.js @@ -1,6 +1,6 @@ var util = require('../util'); -import ColorPicker from './ColorPicker' +var ColorPicker = require('./ColorPicker').default; /** * The way this works is for all properties of this.possible options, you can supply the property name in any form to list the options. diff --git a/lib/timeline/Graph2d.js b/lib/timeline/Graph2d.js index a222456e..c5234c1d 100644 --- a/lib/timeline/Graph2d.js +++ b/lib/timeline/Graph2d.js @@ -15,8 +15,8 @@ var printStyle = require('../shared/Validator').printStyle; var allOptions = require('./optionsGraph2d').allOptions; var configureOptions = require('./optionsGraph2d').configureOptions; -import Configurator from '../shared/Configurator'; -import Validator from '../shared/Validator'; +var Configurator = require('../shared/Configurator').default; +var Validator = require('../shared/Validator').default; /** * Create a timeline visualization diff --git a/lib/timeline/Timeline.js b/lib/timeline/Timeline.js index 8b870578..c8a63f9a 100644 --- a/lib/timeline/Timeline.js +++ b/lib/timeline/Timeline.js @@ -15,8 +15,8 @@ var printStyle = require('../shared/Validator').printStyle; var allOptions = require('./optionsTimeline').allOptions; var configureOptions = require('./optionsTimeline').configureOptions; -import Configurator from '../shared/Configurator'; -import Validator from '../shared/Validator'; +var Configurator = require('../shared/Configurator').default; +var Validator = require('../shared/Validator').default; /** diff --git a/lib/timeline/component/ItemSet.js b/lib/timeline/component/ItemSet.js index 09f16f39..8971d396 100644 --- a/lib/timeline/component/ItemSet.js +++ b/lib/timeline/component/ItemSet.js @@ -10,7 +10,7 @@ var BoxItem = require('./item/BoxItem'); var PointItem = require('./item/PointItem'); var RangeItem = require('./item/RangeItem'); var BackgroundItem = require('./item/BackgroundItem'); -import Popup from '../../shared/Popup'; +var Popup = require('../../shared/Popup').default; var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items From ec4699df57a3f4993e933311a848bd2c2d05461e Mon Sep 17 00:00:00 2001 From: yotamberk Date: Sat, 20 May 2017 12:21:39 +0300 Subject: [PATCH 26/28] Nathanbennett improve timeline stack performance (#3078) * Fix redraw order * Fix error when option is not defined * Allow template labels * Add .travis.yml file * Add experiment travis code * Fix react example * Improve redraw performance by not restacking non-visible groups (related to #2835) * only restack necessary groups on redraw (related to #2835) --- lib/timeline/component/BackgroundGroup.js | 4 +- lib/timeline/component/Group.js | 54 +++++++++++++---------- lib/timeline/component/ItemSet.js | 18 ++------ lib/timeline/component/item/Item.js | 3 +- 4 files changed, 38 insertions(+), 41 deletions(-) diff --git a/lib/timeline/component/BackgroundGroup.js b/lib/timeline/component/BackgroundGroup.js index af1286d5..ae44551c 100644 --- a/lib/timeline/component/BackgroundGroup.js +++ b/lib/timeline/component/BackgroundGroup.js @@ -22,10 +22,10 @@ BackgroundGroup.prototype = Object.create(Group.prototype); * Repaint this group * @param {{start: number, end: number}} range * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin - * @param {boolean} [restack=false] Force restacking of all items + * @param {boolean} [forceRestack=false] Force restacking of all items * @return {boolean} Returns true if the group is resized */ -BackgroundGroup.prototype.redraw = function(range, margin, restack) { +BackgroundGroup.prototype.redraw = function(range, margin, forceRestack) { var resized = false; this.visibleItems = this._updateItemsInRange(this.orderedItems, this.visibleItems, range); diff --git a/lib/timeline/component/Group.js b/lib/timeline/component/Group.js index 58920dfa..095262ea 100644 --- a/lib/timeline/component/Group.js +++ b/lib/timeline/component/Group.js @@ -15,6 +15,7 @@ function Group (groupId, data, itemSet) { this.subgroupOrderer = data && data.subgroupOrder; this.itemSet = itemSet; this.isVisible = null; + this.stackDirty = true; // if true, items will be restacked on next redraw if (data && data.nestedGroups) { this.nestedGroups = data.nestedGroups; @@ -212,12 +213,12 @@ Group.prototype.getLabelWidth = function() { * Repaint this group * @param {{start: number, end: number}} range * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin - * @param {boolean} [restack=false] Force restacking of all items + * @param {boolean} [forceRestack=false] Force restacking of all items * @return {boolean} Returns true if the group is resized */ -Group.prototype.redraw = function(range, margin, restack) { +Group.prototype.redraw = function(range, margin, forceRestack) { var resized = false; - + // force recalculation of the height of the items when the marker height changed // (due to the Timeline being attached to the DOM or changed from display:none to visible) var markerHeight = this.dom.marker.clientHeight; @@ -228,9 +229,9 @@ Group.prototype.redraw = function(range, margin, restack) { if (item.displayed) item.redraw(); }); - restack = true; - } - + forceRestack = true; + } + // recalculate the height of the subgroups this._calculateSubGroupHeights(margin); @@ -240,12 +241,15 @@ Group.prototype.redraw = function(range, margin, restack) { this.right = foreground.offsetLeft; this.width = foreground.offsetWidth; + var lastIsVisible = this.isVisible; this.isVisible = this._isGroupVisible(range, margin); - // reposition visible items vertically - if (typeof this.itemSet.options.order === 'function') { - // a custom order function + + var restack = forceRestack || this.stackDirty || (this.isVisible && !lastIsVisible); - if (restack) { + // if restacking, reposition visible items vertically + if(restack) { + if (typeof this.itemSet.options.order === 'function') { + // a custom order function // brute force restack of all items // show all items @@ -264,23 +268,24 @@ Group.prototype.redraw = function(range, margin, restack) { return me.itemSet.options.order(a.data, b.data); }); stack.stack(customOrderedItems, margin, true /* restack=true */); - } - - this.visibleItems = this._updateItemsInRange(this.orderedItems, this.visibleItems, range); - } - else { - // no custom order function, lazy stacking + this.visibleItems = this._updateItemsInRange(this.orderedItems, this.visibleItems, range); - this.visibleItems = this._updateItemsInRange(this.orderedItems, this.visibleItems, range); - - if (this.itemSet.options.stack) { // TODO: ugly way to access options... - stack.stack(this.visibleItems, margin, restack); } - else { // no stacking - stack.nostack(this.visibleItems, margin, this.subgroups, this.itemSet.options.stackSubgroups); + else { + // no custom order function, lazy stacking + this.visibleItems = this._updateItemsInRange(this.orderedItems, this.visibleItems, range); + + if (this.itemSet.options.stack) { // TODO: ugly way to access options... + stack.stack(this.visibleItems, margin, true /* restack=true */); + } + else { // no stacking + stack.nostack(this.visibleItems, margin, this.subgroups, this.itemSet.options.stackSubgroups); + } } + + this.stackDirty = false; } - + this._updateSubgroupsSizes(); // recalculate the height of the group @@ -435,7 +440,7 @@ Group.prototype.hide = function() { Group.prototype.add = function(item) { this.items[item.id] = item; item.setParent(this); - + this.stackDirty = true; // add to if (item.data.subgroup !== undefined) { this._addToSubgroup(item); @@ -540,6 +545,7 @@ Group.prototype.resetSubgroups = function() { Group.prototype.remove = function(item) { delete this.items[item.id]; item.setParent(null); + this.stackDirty = true; // remove from visible items var index = this.visibleItems.indexOf(item); diff --git a/lib/timeline/component/ItemSet.js b/lib/timeline/component/ItemSet.js index 8971d396..230e8781 100644 --- a/lib/timeline/component/ItemSet.js +++ b/lib/timeline/component/ItemSet.js @@ -155,7 +155,6 @@ function ItemSet(body, options) { this.groupIds = []; this.selection = []; // list with the ids of all selected nodes - this.stackDirty = true; // if true, all items will be restacked on next redraw this.popup = null; @@ -418,7 +417,6 @@ ItemSet.prototype.setOptions = function(options) { */ ItemSet.prototype.markDirty = function(options) { this.groupIds = []; - this.stackDirty = true; if (options && options.refreshItems) { util.forEach(this.items, function (item) { @@ -617,12 +615,11 @@ ItemSet.prototype.redraw = function() { var visibleInterval = range.end - range.start; var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.props.width != this.props.lastWidth); var scrolled = range.start != this.lastRangeStart; - if (zoomed || scrolled) this.stackDirty = true; + var forceRestack = (zoomed || scrolled); this.lastVisibleInterval = visibleInterval; this.lastRangeStart = range.start; this.props.lastWidth = this.props.width; - var restack = this.stackDirty; var firstGroup = this._firstGroup(); var firstMargin = { item: margin.item, @@ -636,17 +633,16 @@ ItemSet.prototype.redraw = function() { var minHeight = margin.axis + margin.item.vertical; // redraw the background group - this.groups[BACKGROUND].redraw(range, nonFirstMargin, restack); + this.groups[BACKGROUND].redraw(range, nonFirstMargin, forceRestack); // redraw all regular groups util.forEach(this.groups, function (group) { var groupMargin = (group == firstGroup) ? firstMargin : nonFirstMargin; - var groupResized = group.redraw(range, groupMargin, restack); + var groupResized = group.redraw(range, groupMargin, forceRestack); resized = groupResized || resized; height += group.height; }); height = Math.max(height, minHeight); - this.stackDirty = false; // update frame height frame.style.height = asSize(height); @@ -978,7 +974,6 @@ ItemSet.prototype._onUpdate = function(ids) { }.bind(this)); this._order(); - this.stackDirty = true; // force re-stacking of all items next redraw this.body.emitter.emit('_change', {queue: true}); }; @@ -1008,7 +1003,6 @@ ItemSet.prototype._onRemove = function(ids) { if (count) { // update order this._order(); - this.stackDirty = true; // force re-stacking of all items next redraw this.body.emitter.emit('_change', {queue: true}); } }; @@ -1540,7 +1534,6 @@ ItemSet.prototype._onDrag = function (event) { //make sure we stay in bounds newOffset = Math.max(0, newOffset); newOffset = Math.min(me.groupIds.length-1, newOffset); - itemData.group = me.groupIds[newOffset]; } } @@ -1553,8 +1546,7 @@ ItemSet.prototype._onDrag = function (event) { } }.bind(this)); }.bind(this)); - - this.stackDirty = true; // force re-stacking of all items next redraw + this.body.emitter.emit('_change'); } }; @@ -1607,7 +1599,6 @@ ItemSet.prototype._onDragEnd = function (event) { } // force re-stacking of all items next redraw - me.stackDirty = true; me.body.emitter.emit('_change'); }); } @@ -1624,7 +1615,6 @@ ItemSet.prototype._onDragEnd = function (event) { // restore original values props.item.setData(props.data); - me.stackDirty = true; // force re-stacking of all items next redraw me.body.emitter.emit('_change'); } }); diff --git a/lib/timeline/component/item/Item.js b/lib/timeline/component/item/Item.js index 30c0bbc8..9bfa662d 100644 --- a/lib/timeline/component/item/Item.js +++ b/lib/timeline/component/item/Item.js @@ -64,6 +64,7 @@ Item.prototype.setData = function(data) { if (groupChanged && this.parent != null) { this.parent.itemSet._moveToGroup(this, data.group); } + this.parent.stackDirty = true; var subGroupChanged = data.subgroup != undefined && this.data.subgroup != data.subgroup; if (subGroupChanged && this.parent != null) { @@ -78,7 +79,7 @@ Item.prototype.setData = function(data) { /** * Set a parent for the item - * @param {ItemSet | Group} parent + * @param {Group} parent */ Item.prototype.setParent = function(parent) { if (this.displayed) { From 585d8b092e1b72633a7a89fc002d8e195a58209a Mon Sep 17 00:00:00 2001 From: Angelo Youn Date: Sat, 20 May 2017 09:58:00 -0700 Subject: [PATCH 27/28] LineGraph: Add an existingItemsMap to check if items are new or not before skipping (#3075) --- lib/timeline/component/LineGraph.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/timeline/component/LineGraph.js b/lib/timeline/component/LineGraph.js index ec797541..f0287bc9 100644 --- a/lib/timeline/component/LineGraph.js +++ b/lib/timeline/component/LineGraph.js @@ -453,6 +453,7 @@ LineGraph.prototype._updateAllGroupData = function (ids, groupIds) { } //Pre-load arrays from existing groups if items are not changed (not in ids) + var existingItemsMap = {}; if (!groupIds && ids) { for (var groupId in this.groups) { if (this.groups.hasOwnProperty(groupId)) { @@ -460,6 +461,7 @@ LineGraph.prototype._updateAllGroupData = function (ids, groupIds) { var existing_items = group.getItems(); groupsContent[groupId] = existing_items.filter(function (item) { + existingItemsMap[item[fieldId]] = item[fieldId]; return (item[fieldId] !== idMap[item[fieldId]]); }); var newLength = groupCounts[groupId]; @@ -478,7 +480,7 @@ LineGraph.prototype._updateAllGroupData = function (ids, groupIds) { if (groupId === null || groupId === undefined) { groupId = UNGROUPED; } - if (!groupIds && ids && (item[fieldId] !== idMap[item[fieldId]])) { + if (!groupIds && ids && (item[fieldId] !== idMap[item[fieldId]]) && existingItemsMap.hasOwnProperty(item[fieldId])) { continue; } if (!groupsContent.hasOwnProperty(groupId)) { From 28fc990661f2d6d01b21956de9085c1bb4a6d589 Mon Sep 17 00:00:00 2001 From: Cameron Kloot Date: Sat, 20 May 2017 13:45:13 -0400 Subject: [PATCH 28/28] Timeline 'showTooltips' option (#3046) * Add 'showTooltips' timeline option * Only show timeline popup if option showTooltips is true * Add 'showTooltips' option to timeline docs * Add tooltips disabled timeline example --- docs/timeline/index.html | 7 +++++++ examples/timeline/items/tooltip.html | 17 ++++++++++++++++- lib/timeline/component/ItemSet.js | 8 +++++--- lib/timeline/optionsTimeline.js | 2 ++ 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/docs/timeline/index.html b/docs/timeline/index.html index da2084f9..293f4cca 100644 --- a/docs/timeline/index.html +++ b/docs/timeline/index.html @@ -1026,6 +1026,13 @@ function (option, path) { visible. + + showTooltips + boolean + true + If true, items with titles will display a tooltip. If false, item tooltips are prevented from showing. + + stack boolean diff --git a/examples/timeline/items/tooltip.html b/examples/timeline/items/tooltip.html index ad2c8723..8908241a 100644 --- a/examples/timeline/items/tooltip.html +++ b/examples/timeline/items/tooltip.html @@ -12,7 +12,7 @@ - + @@ -38,6 +38,12 @@
+

+ Disable item tooltips. +

+ +
+ diff --git a/lib/timeline/component/ItemSet.js b/lib/timeline/component/ItemSet.js index 230e8781..362d4320 100644 --- a/lib/timeline/component/ItemSet.js +++ b/lib/timeline/component/ItemSet.js @@ -95,6 +95,8 @@ function ItemSet(body, options) { axis: 20 }, + showTooltips: true, + tooltip: { followMouse: false, overflowMethod: 'flip' @@ -336,7 +338,7 @@ ItemSet.prototype.setOptions = function(options) { var fields = [ 'type', 'rtl', 'align', 'order', 'stack', 'stackSubgroups', 'selectable', 'multiselect', 'itemsAlwaysDraggable', 'multiselectPerGroup', 'groupOrder', 'dataAttributes', 'template', 'groupTemplate', 'visibleFrameTemplate', - 'hide', 'snap', 'groupOrderSwap', 'tooltip', 'tooltipOnItemUpdateTime' + 'hide', 'snap', 'groupOrderSwap', 'showTooltips', 'tooltip', 'tooltipOnItemUpdateTime' ]; util.selectiveExtend(fields, this.options, options); @@ -1881,7 +1883,7 @@ ItemSet.prototype._onMouseOver = function (event) { } var title = item.getTitle(); - if (title) { + if (this.options.showTooltips && title) { if (this.popup == null) { this.popup = new Popup(this.body.dom.root, this.options.tooltip.overflowMethod || 'flip'); @@ -1931,7 +1933,7 @@ ItemSet.prototype._onMouseMove = function (event) { var item = this.itemFromTarget(event); if (!item) return; - if (this.options.tooltip.followMouse) { + if (this.options.showTooltips && this.options.tooltip.followMouse) { if (this.popup) { if (!this.popup.hidden) { var container = this.body.dom.centerContainer; diff --git a/lib/timeline/optionsTimeline.js b/lib/timeline/optionsTimeline.js index fb6f3c3d..56df9c63 100644 --- a/lib/timeline/optionsTimeline.js +++ b/lib/timeline/optionsTimeline.js @@ -137,6 +137,7 @@ let allOptions = { template: {'function': 'function'}, groupTemplate: {'function': 'function'}, visibleFrameTemplate: {string, 'function': 'function'}, + showTooltips: { 'boolean': bool}, tooltip: { followMouse: { 'boolean': bool }, overflowMethod: { 'string': ['cap', 'flip'] }, @@ -243,6 +244,7 @@ let configureOptions = { // scale: ['millisecond', 'second', 'minute', 'hour', 'weekday', 'day', 'week', 'month', 'year'], // step: [1, 1, 10, 1] //}, + showTooltips: true, tooltip: { followMouse: false, overflowMethod: 'flip'