Browse Source

Merge branch 'develop' of https://github.com/almende/vis into develop

revert-3409-performance
Yotam Berkowitz 7 years ago
parent
commit
af16ffcc18
20 changed files with 530 additions and 195 deletions
  1. +1
    -1
      .github/PULL_REQUEST_TEMPLATE.md
  2. +71
    -0
      HISTORY.md
  3. +31
    -11
      docs/timeline/index.html
  4. +4
    -1
      examples/timeline/interaction/rollingMode.html
  5. +2
    -3
      lib/network/gephiParser.js
  6. +2
    -2
      lib/network/modules/EdgesHandler.js
  7. +93
    -25
      lib/network/modules/KamadaKawai.js
  8. +19
    -7
      lib/network/modules/LayoutEngine.js
  9. +18
    -9
      lib/network/modules/components/algorithms/FloydWarshall.js
  10. +41
    -11
      lib/timeline/Core.js
  11. +60
    -24
      lib/timeline/Range.js
  12. +36
    -37
      lib/timeline/TimeStep.js
  13. +2
    -2
      lib/timeline/Timeline.js
  14. +2
    -2
      lib/timeline/component/CustomTime.js
  15. +71
    -39
      lib/timeline/component/Group.js
  16. +16
    -9
      lib/timeline/component/ItemSet.js
  17. +5
    -1
      lib/timeline/optionsTimeline.js
  18. +1
    -0
      misc/we_need_help.md
  19. +11
    -11
      package.json
  20. +44
    -0
      test/TimeStep.test.js

+ 1
- 1
.github/PULL_REQUEST_TEMPLATE.md View File

@ -3,7 +3,7 @@
Please make sure to check the following requirements before creating a pull request: Please make sure to check the following requirements before creating a pull request:
* [ ] All pull requests must be to the [develop branch](https://github.com/almende/vis/tree/develop). Pull requests to the `master` branch will be closed! * [ ] All pull requests must be to the [develop branch](https://github.com/almende/vis/tree/develop). Pull requests to the `master` branch will be closed!
* [ ] Make sure your changes are based on the latest version of the [develop branch](https://github.com/almende/vis/tree/develop). (Use e.g. `git fetch && git rebase origin develop` to update you feature branch).
* [ ] Make sure your changes are based on the latest version of the [develop branch](https://github.com/almende/vis/tree/develop). (Use e.g. `git fetch && git rebase origin develop` to update your feature branch).
* [ ] Provide an additional or update an example to demonstrate your changes or new features. * [ ] Provide an additional or update an example to demonstrate your changes or new features.
* [ ] Update the documentation if you introduced new behavior or changed existing behavior. * [ ] Update the documentation if you introduced new behavior or changed existing behavior.
* [ ] Reference issue numbers of issues that your pull request addresses. (If you write something like `fixes #1781` in your git commit message this issue gets closed automatically by merging your pull request). * [ ] Reference issue numbers of issues that your pull request addresses. (If you write something like `fixes #1781` in your git commit message this issue gets closed automatically by merging your pull request).

+ 71
- 0
HISTORY.md View File

@ -1,6 +1,77 @@
# vis.js history # vis.js history
http://visjs.org http://visjs.org
## 2017-03-19, version 4.19.1
### General
* FIX: #2685 Fixed babel dependencies (#2875)
### Timeline / Graph2D
* FIX #2809: Fix docs typo in "showNested" (#2879)
* FIX #2594: Fixes for removing and adding items to subgroups (#2821)
* FIX: Allow nested groups to be removed (#2852)
## 2017-03-18, version 4.19.0
### General
- FIX: Fix eslint problem on Travis. (#2744)
- added support for eslint (#2695)
- Trivial typo fix in how_to_help doc. (#2714)
- add link to a mentioned example (#2709)
- FEAT: use babel preset2015 for custom builds (#2678)
- FIX: use babel version compatible with webpack@1.14 (#2693)
- FEAT: run mocha tests in travis ci (#2687)
- Add note that PRs should be submitted against the `develop` branch (#2623)
- FIX: Fixes instanceof Object statements for objects from other windows and iFrames. (#2631)
- removed google-analytics from all examples (#2670)
- do not ignore test folder (#2648)
- updated dependencies and devDependencies (#2649)
- general improvements (#2652)
### Network
- FEAT: Improve the performance of the network layout engine (#2729)
- FEAT: Allow for image nodes to have a selected or broken image (#2601)
### Timeline / Graph2D
- FIX #2842: Prevent redirect to blank after drag and drop in FF (#2871)
- FIX #2810: Nested groups do not use "groupOrder" (#2817)
- FIX #2795: fix date for custom format function (#2826)
- FIX #2689: Add animation options for zoomIn/zoomOut funtions (#2830)
- FIX #2800: Removed all "Object.assign" from examples (#2829)
- FIX #2725: Background items positioning when orientation: top (#2831)
- FEAT: Added data as argument to the template function (#2802)
- FIX #2827: Update "progress bar" example to reflect values (#2828)
- FIX #2672: Item events original event (#2704)
- FIX #2696: Update serialization example to use ISOString dates (#2789)
- FIX #2790: Update examples to use ISOString format (#2791)
- FEAT: Added support to supply an end-time to bar charts to have them scale (#2760)
- FIX #1982, #1417: Modify redraw logic to treat scroll as needing restack (#2774)
- FEAT: Initial tests for timeline ItemSet (#2750)
- FIX #2720: Problems with option editable (#2743, #2796, #2806)
- FIX: Range.js "event" is undeclared (#2749)
- FEAT: added new locales for french and espanol (#2723)
- FIX: fixes timestep next issue (#2732)
- FEAT: #2647 Dynamic rolling mode option (#2705)
- FIX #2679: TypeError: Cannot read property 'hasOwnProperty' of null (#2735)
- Add initial tests for Timeline PointItem (#2716)
- FIX #778: Tooltip does not work with background items in timeline (#2703)
- FIX #2598: Flickering onUpdateTimeTooltip (#2702)
- FEAT: refactor tooltip to only use one dom-element (#2662)
- FEAT: Change setCustomTimeTitle title parameter to be a string or a function (#2611)
### Graph3D
- FEAT #2769: Graph3d tooltip styling (#2780)
- FEAT #2540: Adjusted graph3d doc for autoscaling (#2812)
- FIX #2536: 3d bar graph data array unsorted (#2803)
- FEAT: Added showX(YZ)Axis options to Graph3d (#2686)
## 2017-01-29, version 4.18.1 ## 2017-01-29, version 4.18.1

+ 31
- 11
docs/timeline/index.html View File

@ -468,10 +468,10 @@ var groups = [
<td>Array of group ids nested in the group. Nested groups will appear under this nesting group.</td> <td>Array of group ids nested in the group. Nested groups will appear under this nesting group.</td>
</tr> </tr>
<tr> <tr>
<td>showNestedGroups</td>
<td>showNested</td>
<td>Boolean</td> <td>Boolean</td>
<td>no</td> <td>no</td>
<td>Assuming the group has nested groups, this will set the initial state of the group - shown or collapsed. The <code>showNestedGroups</code> is defaulted to <code>true</code>.</td>
<td>Assuming the group has nested groups, this will set the initial state of the group - shown or collapsed. The <code>showNested</code> is defaulted to <code>true</code>.</td>
</tr> </tr>
</table> </table>
@ -955,13 +955,29 @@ function (option, path) {
<td>Orientation of the timeline items: 'top' or 'bottom' (default). Determines whether items are aligned to the top or bottom of the Timeline.</td> <td>Orientation of the timeline items: 'top' or 'bottom' (default). Determines whether items are aligned to the top or bottom of the Timeline.</td>
</tr> </tr>
<tr>
<td>rollingMode</td>
<tr class='toggle collapsible' onclick="toggleTable('optionTable','rollingMode', this);">
<td><span parent="rollingMode" class="right-caret"></span> rollingMode</td>
<td>Object</td>
<td><code>Object</code></td>
<td>Specify how the timeline implements rolling mode.</td>
</tr>
<tr parent="rollingMode" class="hidden">
<td class="indent">rollingMode.follow</td>
<td>boolean</td> <td>boolean</td>
<td><code>false</code></td> <td><code>false</code></td>
<td>If true, the timeline will initial in a rolling mode - the current time will always be centered. I the user drags the timeline, the timeline will go out of rolling mode and a toggle button will appear. Clicking that button will go back to rolling mode. Zooming in rolling mode will zoom in to the center without consideration of the mouse position.</td> <td>If true, the timeline will initial in a rolling mode - the current time will always be centered. I the user drags the timeline, the timeline will go out of rolling mode and a toggle button will appear. Clicking that button will go back to rolling mode. Zooming in rolling mode will zoom in to the center without consideration of the mouse position.</td>
</tr> </tr>
<tr parent="rollingMode" class="hidden">
<td class="indent">rollingMode.offset</td>
<td>Number</td>
<td><code>'0.5'</code></td>
<td>
Set how far from the left the rolling mode is implemented from. A percentage (i.e. a decimal between 0 and 1)
Defaults to the middle or 0.5 (50%)
</td>
</tr>
<tr> <tr>
<td>rtl</td> <td>rtl</td>
<td>boolean</td> <td>boolean</td>
@ -1055,11 +1071,11 @@ function (option, path) {
This would be used as an additional way to add content that is constant in size with the visible frame of the item and does not get visibly hidden with the item's internal container: <code>vis-item-overflow</code> which is <code>overflow:hidden</code>.</td> This would be used as an additional way to add content that is constant in size with the visible frame of the item and does not get visibly hidden with the item's internal container: <code>vis-item-overflow</code> which is <code>overflow:hidden</code>.</td>
</tr> </tr>
<tr class='depricated'>
<tr class='deprecated'>
<td>throttleRedraw</td> <td>throttleRedraw</td>
<td>number</td> <td>number</td>
<td><code>0</code></td> <td><code>0</code></td>
<td>This option is <b>DEPRICATED</b> and no longer supported. It will be removed in the next MAJOR release.</td>
<td>This option is <b>DEPRECATED</b> and no longer supported. It will be removed in the next MAJOR release.</td>
</tr> </tr>
<tr class='toggle collapsible' onclick="toggleTable('optionTable','timeAxis', this);"> <tr class='toggle collapsible' onclick="toggleTable('optionTable','timeAxis', this);">
@ -1308,12 +1324,13 @@ document.getElementById('myTimeline').onclick = function (event) {
</tr> </tr>
<tr> <tr>
<td>moveTo(time [, options])</td>
<td>moveTo(time [, options, callback])</td>
<td>none</td> <td>none</td>
<td>Move the window such that given time is centered on screen. Parameter <code>time</code> can be a <code>Date</code>, <code>Number</code>, or <code>String</code>. Available options: <td>Move the window such that given time is centered on screen. Parameter <code>time</code> can be a <code>Date</code>, <code>Number</code>, or <code>String</code>. Available options:
<ul> <ul>
<li><code>animation: boolean or {duration: number, easingFunction: string}</code><br>If true (default) or an Object, the range is animated smoothly to the new window. An object can be provided to specify duration and easing function. Default duration is 500 ms, and default easing function is <code>'easeInOutQuad'</code>. Available easing functions: <code>"linear"</code>, <code>"easeInQuad"</code>, <code>"easeOutQuad"</code>, <code>"easeInOutQuad"</code>, <code>"easeInCubic"</code>, <code>"easeOutCubic"</code>, <code>"easeInOutCubic"</code>, <code>"easeInQuart"</code>, <code>"easeOutQuart"</code>, <code>"easeInOutQuart"</code>, <code>"easeInQuint"</code>, <code>"easeOutQuint"</code>, <code>"easeInOutQuint"</code>.</li> <li><code>animation: boolean or {duration: number, easingFunction: string}</code><br>If true (default) or an Object, the range is animated smoothly to the new window. An object can be provided to specify duration and easing function. Default duration is 500 ms, and default easing function is <code>'easeInOutQuad'</code>. Available easing functions: <code>"linear"</code>, <code>"easeInQuad"</code>, <code>"easeOutQuad"</code>, <code>"easeInOutQuad"</code>, <code>"easeInCubic"</code>, <code>"easeOutCubic"</code>, <code>"easeInOutCubic"</code>, <code>"easeInQuart"</code>, <code>"easeOutQuart"</code>, <code>"easeInOutQuart"</code>, <code>"easeInQuint"</code>, <code>"easeOutQuint"</code>, <code>"easeInOutQuint"</code>.</li>
</ul> </ul>
A callback <code>function</code> can be passed as an optional parameter. This function will be called at the end of moveTo function.
</td> </td>
</tr> </tr>
@ -1422,12 +1439,13 @@ document.getElementById('myTimeline').onclick = function (event) {
</tr> </tr>
<tr> <tr>
<td>setWindow(start, end [, options])</td>
<td>setWindow(start, end [, options, callback])</td>
<td>none</td> <td>none</td>
<td>Set the current visible window. The parameters <code>start</code> and <code>end</code> can be a <code>Date</code>, <code>Number</code>, or <code>String</code>. If the parameter value of <code>start</code> or <code>end</code> is null, the parameter will be left unchanged. Available options: <td>Set the current visible window. The parameters <code>start</code> and <code>end</code> can be a <code>Date</code>, <code>Number</code>, or <code>String</code>. If the parameter value of <code>start</code> or <code>end</code> is null, the parameter will be left unchanged. Available options:
<ul> <ul>
<li><code>animation: boolean or {duration: number, easingFunction: string}</code><br>If true (default) or an Object, the range is animated smoothly to the new window. An object can be provided to specify duration and easing function. Default duration is 500 ms, and default easing function is <code>'easeInOutQuad'</code>. Available easing functions: <code>"linear"</code>, <code>"easeInQuad"</code>, <code>"easeOutQuad"</code>, <code>"easeInOutQuad"</code>, <code>"easeInCubic"</code>, <code>"easeOutCubic"</code>, <code>"easeInOutCubic"</code>, <code>"easeInQuart"</code>, <code>"easeOutQuart"</code>, <code>"easeInOutQuart"</code>, <code>"easeInQuint"</code>, <code>"easeOutQuint"</code>, <code>"easeInOutQuint"</code>.</li> <li><code>animation: boolean or {duration: number, easingFunction: string}</code><br>If true (default) or an Object, the range is animated smoothly to the new window. An object can be provided to specify duration and easing function. Default duration is 500 ms, and default easing function is <code>'easeInOutQuad'</code>. Available easing functions: <code>"linear"</code>, <code>"easeInQuad"</code>, <code>"easeOutQuad"</code>, <code>"easeInOutQuad"</code>, <code>"easeInCubic"</code>, <code>"easeOutCubic"</code>, <code>"easeInOutCubic"</code>, <code>"easeInQuart"</code>, <code>"easeOutQuart"</code>, <code>"easeInOutQuart"</code>, <code>"easeInQuint"</code>, <code>"easeOutQuint"</code>, <code>"easeInOutQuint"</code>.</li>
</ul> </ul>
A callback <code>function</code> can be passed as an optional parameter. This function will be called at the end of setWindow function.
</td> </td>
</tr> </tr>
@ -1439,21 +1457,23 @@ document.getElementById('myTimeline').onclick = function (event) {
</tr> </tr>
<tr> <tr>
<td>zoomIn(percentage [, options])</td>
<td>zoomIn(percentage [, options, callback])</td>
<td>none</td> <td>none</td>
<td>Zoom in the current visible window. The parameter <code>percentage</code> can be a <code>Number</code> and must be between 0 and 1. If the parameter value of <code>percentage</code> is null, the window will be left unchanged. Available options: <td>Zoom in the current visible window. The parameter <code>percentage</code> can be a <code>Number</code> and must be between 0 and 1. If the parameter value of <code>percentage</code> is null, the window will be left unchanged. Available options:
<ul> <ul>
<li><code>animation: boolean or {duration: number, easingFunction: string}</code><br>If true (default) or an Object, the range is animated smoothly to the new window. An object can be provided to specify duration and easing function. Default duration is 500 ms, and default easing function is <code>'easeInOutQuad'</code>. Available easing functions: <code>"linear"</code>, <code>"easeInQuad"</code>, <code>"easeOutQuad"</code>, <code>"easeInOutQuad"</code>, <code>"easeInCubic"</code>, <code>"easeOutCubic"</code>, <code>"easeInOutCubic"</code>, <code>"easeInQuart"</code>, <code>"easeOutQuart"</code>, <code>"easeInOutQuart"</code>, <code>"easeInQuint"</code>, <code>"easeOutQuint"</code>, <code>"easeInOutQuint"</code>.</li> <li><code>animation: boolean or {duration: number, easingFunction: string}</code><br>If true (default) or an Object, the range is animated smoothly to the new window. An object can be provided to specify duration and easing function. Default duration is 500 ms, and default easing function is <code>'easeInOutQuad'</code>. Available easing functions: <code>"linear"</code>, <code>"easeInQuad"</code>, <code>"easeOutQuad"</code>, <code>"easeInOutQuad"</code>, <code>"easeInCubic"</code>, <code>"easeOutCubic"</code>, <code>"easeInOutCubic"</code>, <code>"easeInQuart"</code>, <code>"easeOutQuart"</code>, <code>"easeInOutQuart"</code>, <code>"easeInQuint"</code>, <code>"easeOutQuint"</code>, <code>"easeInOutQuint"</code>.</li>
</ul> </ul>
A callback <code>function</code> can be passed as an optional parameter. This function will be called at the end of zoomIn function.
</td> </td>
</tr> </tr>
<tr> <tr>
<td>zoomOut(percentage [, options])</td>
<td>zoomOut(percentage [, options, callback])</td>
<td>none</td> <td>none</td>
<td>Zoom out the current visible window. The parameter <code>percentage</code> can be a <code>Number</code> and must be between 0 and 1. If the parameter value of <code>percentage</code> is null, the window will be left unchanged. Available options: <td>Zoom out the current visible window. The parameter <code>percentage</code> can be a <code>Number</code> and must be between 0 and 1. If the parameter value of <code>percentage</code> is null, the window will be left unchanged. Available options:
<ul> <ul>
<li><code>animation: boolean or {duration: number, easingFunction: string}</code><br>If true (default) or an Object, the range is animated smoothly to the new window. An object can be provided to specify duration and easing function. Default duration is 500 ms, and default easing function is <code>'easeInOutQuad'</code>. Available easing functions: <code>"linear"</code>, <code>"easeInQuad"</code>, <code>"easeOutQuad"</code>, <code>"easeInOutQuad"</code>, <code>"easeInCubic"</code>, <code>"easeOutCubic"</code>, <code>"easeInOutCubic"</code>, <code>"easeInQuart"</code>, <code>"easeOutQuart"</code>, <code>"easeInOutQuart"</code>, <code>"easeInQuint"</code>, <code>"easeOutQuint"</code>, <code>"easeInOutQuint"</code>.</li> <li><code>animation: boolean or {duration: number, easingFunction: string}</code><br>If true (default) or an Object, the range is animated smoothly to the new window. An object can be provided to specify duration and easing function. Default duration is 500 ms, and default easing function is <code>'easeInOutQuad'</code>. Available easing functions: <code>"linear"</code>, <code>"easeInQuad"</code>, <code>"easeOutQuad"</code>, <code>"easeInOutQuad"</code>, <code>"easeInCubic"</code>, <code>"easeOutCubic"</code>, <code>"easeInOutCubic"</code>, <code>"easeInQuart"</code>, <code>"easeOutQuart"</code>, <code>"easeInOutQuart"</code>, <code>"easeInQuint"</code>, <code>"easeOutQuint"</code>, <code>"easeInOutQuint"</code>.</li>
</ul> </ul>
A callback <code>function</code> can be passed as an optional parameter. This function will be called at the end of zoomOut function.
</td> </td>
</tr> </tr>
@ -1991,7 +2011,7 @@ var options = {
<td>Weekday</td><td><code>vis-monday</code>, <code>vis-tuesday</code>, <code>vis-wednesday</code>, <code>vis-thursday</code>, <code>vis-friday</code>, <code>vis-saturday</code>, <code>vis-sunday</code></td> <td>Weekday</td><td><code>vis-monday</code>, <code>vis-tuesday</code>, <code>vis-wednesday</code>, <code>vis-thursday</code>, <code>vis-friday</code>, <code>vis-saturday</code>, <code>vis-sunday</code></td>
</tr> </tr>
<tr> <tr>
<td>Days</td><td><code>vis-date1</code>, <code>vis-date2</code>, ..., <code>vis-date31</code></td>
<td>Days</td><td><code>vis-day1</code>, <code>vis-day2</code>, ..., <code>vis-day31</code></td>
</tr> </tr>
<tr> <tr>
<td>Months</td><td><code>vis-january</code>, <code>vis-february</code>, <code>vis-march</code>, <code>vis-april</code>, <code>vis-may</code>, <code>vis-june</code>, <code>vis-july</code>, <code>vis-august</code>, <code>vis-september</code>, <code>vis-october</code>, <code>vis-november</code>, <code>vis-december</code></td> <td>Months</td><td><code>vis-january</code>, <code>vis-february</code>, <code>vis-march</code>, <code>vis-april</code>, <code>vis-may</code>, <code>vis-june</code>, <code>vis-july</code>, <code>vis-august</code>, <code>vis-september</code>, <code>vis-october</code>, <code>vis-november</code>, <code>vis-december</code></td>

+ 4
- 1
examples/timeline/interaction/rollingMode.html View File

@ -33,7 +33,10 @@
var options = { var options = {
start: new Date(), start: new Date(),
end: new Date(new Date().getTime() + 1000000), end: new Date(new Date().getTime() + 1000000),
rollingMode: true
rollingMode: {
follow: true,
offset: 0.5
}
}; };
// create a Timeline // create a Timeline

+ 2
- 3
lib/network/gephiParser.js View File

@ -45,11 +45,10 @@ function parseGephi(gephiJSON, optionsObj) {
var gNode = gNodes[i]; var gNode = gNodes[i];
node['id'] = gNode.id; node['id'] = gNode.id;
node['attributes'] = gNode.attributes; node['attributes'] = gNode.attributes;
node['title'] = gNode.title;
node['x'] = gNode.x; node['x'] = gNode.x;
node['y'] = gNode.y; node['y'] = gNode.y;
node['label'] = gNode.label; node['label'] = gNode.label;
node['title'] = gNode.attributes !== undefined ? gNode.attributes.title : undefined;
node['title'] = gNode.attributes !== undefined ? gNode.attributes.title : gNode.title;
if (options.nodes.parseColor === true) { if (options.nodes.parseColor === true) {
node['color'] = gNode.color; node['color'] = gNode.color;
} }
@ -64,4 +63,4 @@ function parseGephi(gephiJSON, optionsObj) {
return {nodes:nodes, edges:edges}; return {nodes:nodes, edges:edges};
} }
exports.parseGephi = parseGephi;
exports.parseGephi = parseGephi;

+ 2
- 2
lib/network/modules/EdgesHandler.js View File

@ -385,8 +385,8 @@ class EdgesHandler {
let nodeList = []; let nodeList = [];
if (this.body.edges[edgeId] !== undefined) { if (this.body.edges[edgeId] !== undefined) {
let edge = this.body.edges[edgeId]; let edge = this.body.edges[edgeId];
if (edge.fromId) {nodeList.push(edge.fromId);}
if (edge.toId) {nodeList.push(edge.toId);}
if (edge.fromId !== undefined) {nodeList.push(edge.fromId);}
if (edge.toId !== undefined) {nodeList.push(edge.toId);}
} }
return nodeList; return nodeList;
} }

+ 93
- 25
lib/network/modules/KamadaKawai.js View File

@ -49,11 +49,14 @@ class KamadaKawai {
// get the K Matrix // get the K Matrix
this._createK_matrix(D_matrix); this._createK_matrix(D_matrix);
// initial E Matrix
this._createE_matrix();
// calculate positions // calculate positions
let threshold = 0.01; let threshold = 0.01;
let innerThreshold = 1; let innerThreshold = 1;
let iterations = 0; let iterations = 0;
let maxIterations = Math.max(1000,Math.min(10*this.body.nodeIndices.length,6000));
let maxIterations = Math.max(1000, Math.min(10 * this.body.nodeIndices.length, 6000));
let maxInnerIterations = 5; let maxInnerIterations = 5;
let maxEnergy = 1e9; let maxEnergy = 1e9;
@ -64,10 +67,10 @@ class KamadaKawai {
[highE_nodeId, maxEnergy, dE_dx, dE_dy] = this._getHighestEnergyNode(ignoreClusters); [highE_nodeId, maxEnergy, dE_dx, dE_dy] = this._getHighestEnergyNode(ignoreClusters);
delta_m = maxEnergy; delta_m = maxEnergy;
subIterations = 0; subIterations = 0;
while(delta_m > innerThreshold && subIterations < maxInnerIterations) {
while (delta_m > innerThreshold && subIterations < maxInnerIterations) {
subIterations += 1; subIterations += 1;
this._moveNode(highE_nodeId, dE_dx, dE_dy); this._moveNode(highE_nodeId, dE_dx, dE_dy);
[delta_m,dE_dx,dE_dy] = this._getEnergy(highE_nodeId);
[delta_m, dE_dx, dE_dy] = this._getEnergy(highE_nodeId);
} }
} }
} }
@ -87,7 +90,7 @@ class KamadaKawai {
for (let nodeIdx = 0; nodeIdx < nodesArray.length; nodeIdx++) { for (let nodeIdx = 0; nodeIdx < nodesArray.length; nodeIdx++) {
let m = nodesArray[nodeIdx]; let m = nodesArray[nodeIdx];
// by not evaluating nodes with predefined positions we should only move nodes that have no positions. // by not evaluating nodes with predefined positions we should only move nodes that have no positions.
if ((nodes[m].predefinedPosition === false || nodes[m].isCluster === true && ignoreClusters === true) || nodes[m].options.fixed.x === true || nodes[m].options.fixed.y === true) {
if ((nodes[m].predefinedPosition === false || nodes[m].isCluster === true && ignoreClusters === true) || nodes[m].options.fixed.x === true || nodes[m].options.fixed.y === true) {
let [delta_m,dE_dx,dE_dy] = this._getEnergy(m); let [delta_m,dE_dx,dE_dy] = this._getEnergy(m);
if (maxEnergy < delta_m) { if (maxEnergy < delta_m) {
maxEnergy = delta_m; maxEnergy = delta_m;
@ -108,24 +111,7 @@ class KamadaKawai {
* @private * @private
*/ */
_getEnergy(m) { _getEnergy(m) {
let nodesArray = this.body.nodeIndices;
let nodes = this.body.nodes;
let x_m = nodes[m].x;
let y_m = nodes[m].y;
let dE_dx = 0;
let dE_dy = 0;
for (let iIdx = 0; iIdx < nodesArray.length; iIdx++) {
let i = nodesArray[iIdx];
if (i !== m) {
let x_i = nodes[i].x;
let y_i = nodes[i].y;
let denominator = 1.0 / Math.sqrt(Math.pow(x_m - x_i, 2) + Math.pow(y_m - y_i, 2));
dE_dx += this.K_matrix[m][i] * ((x_m - x_i) - this.L_matrix[m][i] * (x_m - x_i) * denominator);
dE_dy += this.K_matrix[m][i] * ((y_m - y_i) - this.L_matrix[m][i] * (y_m - y_i) * denominator);
}
}
let [dE_dx,dE_dy] = this.E_sums[m];
let delta_m = Math.sqrt(Math.pow(dE_dx, 2) + Math.pow(dE_dy, 2)); let delta_m = Math.sqrt(Math.pow(dE_dx, 2) + Math.pow(dE_dy, 2));
return [delta_m, dE_dx, dE_dy]; return [delta_m, dE_dx, dE_dy];
} }
@ -147,15 +133,20 @@ class KamadaKawai {
let x_m = nodes[m].x; let x_m = nodes[m].x;
let y_m = nodes[m].y; let y_m = nodes[m].y;
let km = this.K_matrix[m];
let lm = this.L_matrix[m];
for (let iIdx = 0; iIdx < nodesArray.length; iIdx++) { for (let iIdx = 0; iIdx < nodesArray.length; iIdx++) {
let i = nodesArray[iIdx]; let i = nodesArray[iIdx];
if (i !== m) { if (i !== m) {
let x_i = nodes[i].x; let x_i = nodes[i].x;
let y_i = nodes[i].y; let y_i = nodes[i].y;
let kmat = km[i];
let lmat = lm[i];
let denominator = 1.0 / Math.pow(Math.pow(x_m - x_i, 2) + Math.pow(y_m - y_i, 2), 1.5); let denominator = 1.0 / Math.pow(Math.pow(x_m - x_i, 2) + Math.pow(y_m - y_i, 2), 1.5);
d2E_dx2 += this.K_matrix[m][i] * (1 - this.L_matrix[m][i] * Math.pow(y_m - y_i, 2) * denominator);
d2E_dxdy += this.K_matrix[m][i] * (this.L_matrix[m][i] * (x_m - x_i) * (y_m - y_i) * denominator);
d2E_dy2 += this.K_matrix[m][i] * (1 - this.L_matrix[m][i] * Math.pow(x_m - x_i, 2) * denominator);
d2E_dx2 += kmat * (1 - lmat * Math.pow(y_m - y_i, 2) * denominator);
d2E_dxdy += kmat * (lmat * (x_m - x_i) * (y_m - y_i) * denominator);
d2E_dy2 += kmat * (1 - lmat * Math.pow(x_m - x_i, 2) * denominator);
} }
} }
// make the variable names easier to make the solving of the linear system easier to read // make the variable names easier to make the solving of the linear system easier to read
@ -168,6 +159,9 @@ class KamadaKawai {
// move the node // move the node
nodes[m].x += dx; nodes[m].x += dx;
nodes[m].y += dy; nodes[m].y += dy;
// Recalculate E_matrix (should be incremental)
this._updateE_matrix(m);
} }
@ -208,8 +202,82 @@ class KamadaKawai {
} }
} }
/**
* Create matrix with all energies between nodes
* @private
*/
_createE_matrix() {
let nodesArray = this.body.nodeIndices;
let nodes = this.body.nodes;
this.E_matrix = {};
this.E_sums = {};
for (let mIdx = 0; mIdx < nodesArray.length; mIdx++) {
this.E_matrix[nodesArray[mIdx]] = [];
}
for (let mIdx = 0; mIdx < nodesArray.length; mIdx++) {
let m = nodesArray[mIdx];
let x_m = nodes[m].x;
let y_m = nodes[m].y;
let dE_dx = 0;
let dE_dy = 0;
for (let iIdx = mIdx; iIdx < nodesArray.length; iIdx++) {
let i = nodesArray[iIdx];
if (i !== m) {
let x_i = nodes[i].x;
let y_i = nodes[i].y;
let denominator = 1.0 / Math.sqrt(Math.pow(x_m - x_i, 2) + Math.pow(y_m - y_i, 2));
this.E_matrix[m][iIdx] = [
this.K_matrix[m][i] * ((x_m - x_i) - this.L_matrix[m][i] * (x_m - x_i) * denominator),
this.K_matrix[m][i] * ((y_m - y_i) - this.L_matrix[m][i] * (y_m - y_i) * denominator)
];
this.E_matrix[i][mIdx] = this.E_matrix[m][iIdx];
dE_dx += this.E_matrix[m][iIdx][0];
dE_dy += this.E_matrix[m][iIdx][1];
}
}
//Store sum
this.E_sums[m] = [dE_dx, dE_dy];
}
}
//Update method, just doing single column (rows are auto-updated) (update all sums)
_updateE_matrix(m) {
let nodesArray = this.body.nodeIndices;
let nodes = this.body.nodes;
let colm = this.E_matrix[m];
let kcolm = this.K_matrix[m];
let lcolm = this.L_matrix[m];
let x_m = nodes[m].x;
let y_m = nodes[m].y;
let dE_dx = 0;
let dE_dy = 0;
for (let iIdx = 0; iIdx < nodesArray.length; iIdx++) {
let i = nodesArray[iIdx];
if (i !== m) {
//Keep old energy value for sum modification below
let cell = colm[iIdx];
let oldDx = cell[0];
let oldDy = cell[1];
//Calc new energy:
let x_i = nodes[i].x;
let y_i = nodes[i].y;
let denominator = 1.0 / Math.sqrt(Math.pow(x_m - x_i, 2) + Math.pow(y_m - y_i, 2));
let dx = kcolm[i] * ((x_m - x_i) - lcolm[i] * (x_m - x_i) * denominator);
let dy = kcolm[i] * ((y_m - y_i) - lcolm[i] * (y_m - y_i) * denominator);
colm[iIdx] = [dx, dy];
dE_dx += dx;
dE_dy += dy;
//add new energy to sum of each column
let sum = this.E_sums[i];
sum[0] += (dx-oldDx);
sum[1] += (dy-oldDy);
}
}
//Store sum at -1 index
this.E_sums[m] = [dE_dx, dE_dy];
}
} }
export default KamadaKawai; export default KamadaKawai;

+ 19
- 7
lib/network/modules/LayoutEngine.js View File

@ -161,9 +161,9 @@ class LayoutEngine {
positionInitially(nodesArray) { positionInitially(nodesArray) {
if (this.options.hierarchical.enabled !== true) { if (this.options.hierarchical.enabled !== true) {
this.randomSeed = this.initialRandomSeed; this.randomSeed = this.initialRandomSeed;
let radius = nodesArray.length + 50;
for (let i = 0; i < nodesArray.length; i++) { for (let i = 0; i < nodesArray.length; i++) {
let node = nodesArray[i]; let node = nodesArray[i];
let radius = 10 * 0.1 * nodesArray.length + 10;
let angle = 2 * Math.PI * this.seededRandom(); let angle = 2 * Math.PI * this.seededRandom();
if (node.x === undefined) { if (node.x === undefined) {
node.x = radius * Math.cos(angle); node.x = radius * Math.cos(angle);
@ -196,34 +196,46 @@ class LayoutEngine {
if (positionDefined < 0.5 * this.body.nodeIndices.length) { if (positionDefined < 0.5 * this.body.nodeIndices.length) {
let MAX_LEVELS = 10; let MAX_LEVELS = 10;
let level = 0; let level = 0;
let clusterThreshold = 100;
let clusterThreshold = 150;
//Performance enhancement, during clustering edges need only be simple straight lines. These options don't propagate outside the clustering phase.
let clusterOptions = {
clusterEdgeProperties:{
smooth: {
enabled: false
}
}
};
// if there are a lot of nodes, we cluster before we run the algorithm. // if there are a lot of nodes, we cluster before we run the algorithm.
if (this.body.nodeIndices.length > clusterThreshold) { if (this.body.nodeIndices.length > clusterThreshold) {
let startLength = this.body.nodeIndices.length; let startLength = this.body.nodeIndices.length;
while (this.body.nodeIndices.length > clusterThreshold) {
while (this.body.nodeIndices.length > clusterThreshold && level <= MAX_LEVELS) {
//console.time("clustering") //console.time("clustering")
level += 1; level += 1;
let before = this.body.nodeIndices.length; let before = this.body.nodeIndices.length;
// if there are many nodes we do a hubsize cluster // if there are many nodes we do a hubsize cluster
if (level % 3 === 0) { if (level % 3 === 0) {
this.body.modules.clustering.clusterBridges();
this.body.modules.clustering.clusterBridges(clusterOptions);
} }
else { else {
this.body.modules.clustering.clusterOutliers();
this.body.modules.clustering.clusterOutliers(clusterOptions);
} }
let after = this.body.nodeIndices.length; let after = this.body.nodeIndices.length;
if ((before == after && level % 3 !== 0) || level > MAX_LEVELS) {
if (before == after && level % 3 !== 0) {
this._declusterAll(); this._declusterAll();
this.body.emitter.emit("_layoutFailed"); this.body.emitter.emit("_layoutFailed");
console.info("This network could not be positioned by this version of the improved layout algorithm. Please disable improvedLayout for better performance."); console.info("This network could not be positioned by this version of the improved layout algorithm. Please disable improvedLayout for better performance.");
return; return;
} }
//console.timeEnd("clustering") //console.timeEnd("clustering")
//console.log(level,after)
//console.log(before,level,after);
} }
// increase the size of the edges // increase the size of the edges
this.body.modules.kamadaKawai.setOptions({springLength: Math.max(150, 2 * startLength)}) this.body.modules.kamadaKawai.setOptions({springLength: Math.max(150, 2 * startLength)})
} }
if (level > MAX_LEVELS){
console.info("The clustering didn't succeed within the amount of interations allowed, progressing with partial result.");
}
// position the system for these nodes and edges // position the system for these nodes and edges
this.body.modules.kamadaKawai.solve(this.body.nodeIndices, this.body.edgeIndices, true); this.body.modules.kamadaKawai.solve(this.body.nodeIndices, this.body.edgeIndices, true);

+ 18
- 9
lib/network/modules/components/algorithms/FloydWarshall.js View File

@ -4,7 +4,8 @@
class FloydWarshall { class FloydWarshall {
constructor(){}
constructor() {
}
getDistances(body, nodesArray, edgesArray) { getDistances(body, nodesArray, edgesArray) {
let D_matrix = {}; let D_matrix = {};
@ -12,11 +13,11 @@ class FloydWarshall {
// prepare matrix with large numbers // prepare matrix with large numbers
for (let i = 0; i < nodesArray.length; i++) { for (let i = 0; i < nodesArray.length; i++) {
D_matrix[nodesArray[i]] = {};
D_matrix[nodesArray[i]] = {};
let node = nodesArray[i];
let cell = {};
D_matrix[node] = cell;
for (let j = 0; j < nodesArray.length; j++) { for (let j = 0; j < nodesArray.length; j++) {
D_matrix[nodesArray[i]][nodesArray[j]] = (i == j ? 0 : 1e9);
D_matrix[nodesArray[i]][nodesArray[j]] = (i == j ? 0 : 1e9);
cell[nodesArray[j]] = (i == j ? 0 : 1e9);
} }
} }
@ -34,10 +35,18 @@ class FloydWarshall {
// Adapted FloydWarshall based on unidirectionality to greatly reduce complexity. // Adapted FloydWarshall based on unidirectionality to greatly reduce complexity.
for (let k = 0; k < nodeCount; k++) { for (let k = 0; k < nodeCount; k++) {
for (let i = 0; i < nodeCount-1; i++) {
for (let j = i+1; j < nodeCount; j++) {
D_matrix[nodesArray[i]][nodesArray[j]] = Math.min(D_matrix[nodesArray[i]][nodesArray[j]],D_matrix[nodesArray[i]][nodesArray[k]] + D_matrix[nodesArray[k]][nodesArray[j]])
D_matrix[nodesArray[j]][nodesArray[i]] = D_matrix[nodesArray[i]][nodesArray[j]];
let knode = nodesArray[k];
let kcolm = D_matrix[knode];
for (let i = 0; i < nodeCount - 1; i++) {
let inode = nodesArray[i];
let icolm = D_matrix[inode];
for (let j = i + 1; j < nodeCount; j++) {
let jnode = nodesArray[j];
let jcolm = D_matrix[jnode];
let val = Math.min(icolm[jnode], icolm[knode] + kcolm[jnode]);
icolm[jnode] = val;
jcolm[inode] = val;
} }
} }
} }

+ 41
- 11
lib/timeline/Core.js View File

@ -245,6 +245,9 @@ Core.prototype._create = function (container) {
} }
function handleDrop(event) { function handleDrop(event) {
// prevent redirect to blank page - Firefox
if(event.preventDefault) { event.preventDefault(); }
if(event.stopPropagation) { event.stopPropagation(); }
// return when dropping non-vis items // return when dropping non-vis items
try { try {
var itemData = JSON.parse(event.dataTransfer.getData("text")) var itemData = JSON.parse(event.dataTransfer.getData("text"))
@ -610,8 +613,9 @@ Core.prototype.getVisibleItems = function() {
* provided to specify duration and easing function. * provided to specify duration and easing function.
* Default duration is 500 ms, and default easing * Default duration is 500 ms, and default easing
* function is 'easeInOutQuad'. * function is 'easeInOutQuad'.
* @param {Function} a callback funtion to be executed at the end of this function
*/ */
Core.prototype.fit = function(options) {
Core.prototype.fit = function(options, callback) {
var range = this.getDataRange(); var range = this.getDataRange();
// skip range set if there is no min and max date // skip range set if there is no min and max date
@ -624,7 +628,7 @@ Core.prototype.fit = function(options) {
var min = new Date(range.min.valueOf() - interval * 0.01); var min = new Date(range.min.valueOf() - interval * 0.01);
var max = new Date(range.max.valueOf() + interval * 0.01); var max = new Date(range.max.valueOf() + interval * 0.01);
var animation = (options && options.animation !== undefined) ? options.animation : true; var animation = (options && options.animation !== undefined) ? options.animation : true;
this.range.setRange(min, max, animation);
this.range.setRange(min, max, { animation: animation }, callback);
}; };
/** /**
@ -657,17 +661,28 @@ Core.prototype.getDataRange = function() {
* provided to specify duration and easing function. * provided to specify duration and easing function.
* Default duration is 500 ms, and default easing * Default duration is 500 ms, and default easing
* function is 'easeInOutQuad'. * function is 'easeInOutQuad'.
* @param {Function} a callback funtion to be executed at the end of this function
*/ */
Core.prototype.setWindow = function(start, end, options) {
Core.prototype.setWindow = function(start, end, options, callback) {
if (typeof arguments[2] == "function") {
callback = arguments[2]
options = {};
}
var animation; var animation;
if (arguments.length == 1) { if (arguments.length == 1) {
var range = arguments[0]; var range = arguments[0];
animation = (range.animation !== undefined) ? range.animation : true; animation = (range.animation !== undefined) ? range.animation : true;
this.range.setRange(range.start, range.end, animation);
this.range.setRange(range.start, range.end, { animation: animation });
}
else if (arguments.length == 2 && typeof arguments[1] == "function") {
var range = arguments[0];
callback = arguments[1];
animation = (range.animation !== undefined) ? range.animation : true;
this.range.setRange(range.start, range.end, { animation: animation }, callback);
} }
else { else {
animation = (options && options.animation !== undefined) ? options.animation : true; animation = (options && options.animation !== undefined) ? options.animation : true;
this.range.setRange(start, end, animation);
this.range.setRange(start, end, { animation: animation }, callback);
} }
}; };
@ -681,8 +696,13 @@ Core.prototype.setWindow = function(start, end, options) {
* provided to specify duration and easing function. * provided to specify duration and easing function.
* Default duration is 500 ms, and default easing * Default duration is 500 ms, and default easing
* function is 'easeInOutQuad'. * function is 'easeInOutQuad'.
* @param {Function} a callback funtion to be executed at the end of this function
*/ */
Core.prototype.moveTo = function(time, options) {
Core.prototype.moveTo = function(time, options, callback) {
if (typeof arguments[1] == "function") {
callback = arguments[1]
options = {};
}
var interval = this.range.end - this.range.start; var interval = this.range.end - this.range.start;
var t = util.convert(time, 'Date').valueOf(); var t = util.convert(time, 'Date').valueOf();
@ -690,7 +710,7 @@ Core.prototype.moveTo = function(time, options) {
var end = t + interval / 2; var end = t + interval / 2;
var animation = (options && options.animation !== undefined) ? options.animation : true; var animation = (options && options.animation !== undefined) ? options.animation : true;
this.range.setRange(start, end, animation);
this.range.setRange(start, end, { animation: animation }, callback);
}; };
/** /**
@ -715,9 +735,14 @@ Core.prototype.getWindow = function() {
* provided to specify duration and easing function. * provided to specify duration and easing function.
* Default duration is 500 ms, and default easing * Default duration is 500 ms, and default easing
* function is 'easeInOutQuad'. * function is 'easeInOutQuad'.
* @param {Function} a callback funtion to be executed at the end of this function
*/ */
Core.prototype.zoomIn = function(percentage, options) {
Core.prototype.zoomIn = function(percentage, options, callback) {
if (!percentage || percentage < 0 || percentage > 1) return if (!percentage || percentage < 0 || percentage > 1) return
if (typeof arguments[1] == "function") {
callback = arguments[1]
options = {};
}
var range = this.getWindow(); var range = this.getWindow();
var start = range.start.valueOf(); var start = range.start.valueOf();
var end = range.end.valueOf(); var end = range.end.valueOf();
@ -727,7 +752,7 @@ Core.prototype.zoomIn = function(percentage, options) {
var newStart = start + distance; var newStart = start + distance;
var newEnd = end - distance; var newEnd = end - distance;
this.setWindow(newStart, newEnd, options);
this.setWindow(newStart, newEnd, options, callback);
}; };
/** /**
@ -740,9 +765,14 @@ Core.prototype.zoomIn = function(percentage, options) {
* provided to specify duration and easing function. * provided to specify duration and easing function.
* Default duration is 500 ms, and default easing * Default duration is 500 ms, and default easing
* function is 'easeInOutQuad'. * function is 'easeInOutQuad'.
* @param {Function} a callback funtion to be executed at the end of this function
*/ */
Core.prototype.zoomOut = function(percentage, options) {
Core.prototype.zoomOut = function(percentage, options, callback) {
if (!percentage || percentage < 0 || percentage > 1) return if (!percentage || percentage < 0 || percentage > 1) return
if (typeof arguments[1] == "function") {
callback = arguments[1]
options = {};
}
var range = this.getWindow(); var range = this.getWindow();
var start = range.start.valueOf(); var start = range.start.valueOf();
var end = range.end.valueOf(); var end = range.end.valueOf();
@ -750,7 +780,7 @@ Core.prototype.zoomOut = function(percentage, options) {
var newStart = start - interval * percentage / 2; var newStart = start - interval * percentage / 2;
var newEnd = end + interval * percentage / 2; var newEnd = end + interval * percentage / 2;
this.setWindow(newStart, newEnd, options);
this.setWindow(newStart, newEnd, options, callback);
}; };
/** /**

+ 60
- 24
lib/timeline/Range.js View File

@ -45,7 +45,11 @@ function Range(body, options) {
min: null, min: null,
max: null, max: null,
zoomMin: 10, // milliseconds zoomMin: 10, // milliseconds
zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000 // milliseconds
zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
rollingMode: {
follow: false,
offset: 0.5
}
}; };
this.options = util.extend({}, this.defaultOptions); this.options = util.extend({}, this.defaultOptions);
this.props = { this.props = {
@ -94,11 +98,11 @@ Range.prototype.setOptions = function (options) {
// copy the options that we know // copy the options that we know
var fields = [ var fields = [
'animation', 'direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable', 'animation', 'direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable',
'moment', 'activate', 'hiddenDates', 'zoomKey', 'rtl', 'showCurrentTime', 'rollMode', 'horizontalScroll'
'moment', 'activate', 'hiddenDates', 'zoomKey', 'rtl', 'showCurrentTime', 'rollingMode', 'horizontalScroll'
]; ];
util.selectiveExtend(fields, this.options, options); util.selectiveExtend(fields, this.options, options);
if (options.rollingMode) {
if (options.rollingMode && options.rollingMode.follow) {
this.startRolling(); this.startRolling();
} }
if ('start' in options || 'end' in options) { if ('start' in options || 'end' in options) {
@ -134,11 +138,14 @@ Range.prototype.startRolling = function() {
var interval = me.end - me.start; var interval = me.end - me.start;
var t = util.convert(new Date(), 'Date').valueOf(); var t = util.convert(new Date(), 'Date').valueOf();
var start = t - interval / 2;
var end = t + interval / 2;
var start = t - interval * (me.options.rollingMode.offset);
var end = t + interval * (1 - me.options.rollingMode.offset);
var animation = (me.options && me.options.animation !== undefined) ? me.options.animation : true; var animation = (me.options && me.options.animation !== undefined) ? me.options.animation : true;
me.setRange(start, end, false);
var options = {
animation: false
};
me.setRange(start, end, options);
// determine interval to refresh // determine interval to refresh
var scale = me.conversion(me.body.domProps.center.width).scale; var scale = me.conversion(me.body.domProps.center.width).scale;
@ -169,29 +176,36 @@ Range.prototype.stopRolling = function() {
* Set a new start and end range * Set a new start and end range
* @param {Date | Number | String} [start] * @param {Date | Number | String} [start]
* @param {Date | Number | String} [end] * @param {Date | Number | String} [end]
* @param {boolean | {duration: number, easingFunction: string}} [animation=false]
* If true (default), the range is animated
* @param {Object} options Available options:
* {Boolean | {duration: number, easingFunction: string}} [animation=false]
* If true, the range is animated
* smoothly to the new window. An object can be * smoothly to the new window. An object can be
* provided to specify duration and easing function. * provided to specify duration and easing function.
* Default duration is 500 ms, and default easing * Default duration is 500 ms, and default easing
* function is 'easeInOutQuad'. * function is 'easeInOutQuad'.
* @param {Boolean} [byUser=false]
* {Boolean} [byUser=false]
* {Event} event Mouse event
* {Function} a callback funtion to be executed at the end of this function
* *
*/ */
Range.prototype.setRange = function(start, end, animation, byUser, event) {
if (byUser !== true) {
byUser = false;
Range.prototype.setRange = function(start, end, options, callback) {
if (!options) {
options = {};
}
if (options.byUser !== true) {
options.byUser = false;
} }
var finalStart = start != undefined ? util.convert(start, 'Date').valueOf() : null; var finalStart = start != undefined ? util.convert(start, 'Date').valueOf() : null;
var finalEnd = end != undefined ? util.convert(end, 'Date').valueOf() : null; var finalEnd = end != undefined ? util.convert(end, 'Date').valueOf() : null;
this._cancelAnimation(); this._cancelAnimation();
if (animation) { // true or an Object
if (options.animation) { // true or an Object
var me = this; var me = this;
var initStart = this.start; var initStart = this.start;
var initEnd = this.end; var initEnd = this.end;
var duration = (typeof animation === 'object' && 'duration' in animation) ? animation.duration : 500;
var easingName = (typeof animation === 'object' && 'easingFunction' in animation) ? animation.easingFunction : 'easeInOutQuad';
var duration = (typeof options.animation === 'object' && 'duration' in options.animation) ? options.animation.duration : 500;
var easingName = (typeof options.animation === 'object' && 'easingFunction' in options.animation) ? options.animation.easingFunction : 'easeInOutQuad';
var easingFunction = util.easingFunctions[easingName]; var easingFunction = util.easingFunctions[easingName];
if (!easingFunction) { if (!easingFunction) {
throw new Error('Unknown easing function ' + JSON.stringify(easingName) + '. ' + throw new Error('Unknown easing function ' + JSON.stringify(easingName) + '. ' +
@ -217,8 +231,8 @@ Range.prototype.setRange = function(start, end, animation, byUser, event) {
var params = { var params = {
start: new Date(me.start), start: new Date(me.start),
end: new Date(me.end), end: new Date(me.end),
byUser:byUser,
event: event
byUser: options.byUser,
event: options.event
} }
if (changed) { if (changed) {
@ -228,6 +242,7 @@ Range.prototype.setRange = function(start, end, animation, byUser, event) {
if (done) { if (done) {
if (anyChanged) { if (anyChanged) {
me.body.emitter.emit('rangechanged', params); me.body.emitter.emit('rangechanged', params);
if (callback) { return callback() }
} }
} }
else { else {
@ -247,11 +262,12 @@ Range.prototype.setRange = function(start, end, animation, byUser, event) {
var params = { var params = {
start: new Date(this.start), start: new Date(this.start),
end: new Date(this.end), end: new Date(this.end),
byUser:byUser,
event: event
byUser: options.byUser,
event: options.event
}; };
this.body.emitter.emit('rangechange', params); this.body.emitter.emit('rangechange', params);
this.body.emitter.emit('rangechanged', params); this.body.emitter.emit('rangechanged', params);
if (callback) { return callback() }
} }
} }
}; };
@ -600,7 +616,12 @@ Range.prototype._onMouseWheel = function(event) {
var newStart = this.start - diff; var newStart = this.start - diff;
var newEnd = this.end - diff; var newEnd = this.end - diff;
this.setRange(newStart, newEnd, false, true, event);
var options = {
animation: false,
byUser: true,
event: event
}
this.setRange(newStart, newEnd, options);
} }
return; return;
} }
@ -630,7 +651,7 @@ Range.prototype._onMouseWheel = function(event) {
// calculate center, the date to zoom around // calculate center, the date to zoom around
var pointerDate var pointerDate
if (this.rolling) { if (this.rolling) {
pointerDate = (this.start + this.end) / 2;
pointerDate = this.start + ((this.end - this.start) * this.options.rollingMode.offset);
} else { } else {
var pointer = this.getPointer({x: event.clientX, y: event.clientY}, this.body.dom.center); var pointer = this.getPointer({x: event.clientX, y: event.clientY}, this.body.dom.center);
pointerDate = this._pointerToDate(pointer); pointerDate = this._pointerToDate(pointer);
@ -698,7 +719,12 @@ Range.prototype._onPinch = function (event) {
newEnd = safeEnd; newEnd = safeEnd;
} }
this.setRange(newStart, newEnd, false, true, event);
var options = {
animation: false,
byUser: true,
event: event
}
this.setRange(newStart, newEnd, options);
this.startToFront = false; // revert to default this.startToFront = false; // revert to default
this.endToFront = true; // revert to default this.endToFront = true; // revert to default
@ -802,7 +828,12 @@ Range.prototype.zoom = function(scale, center, delta, event) {
newEnd = safeEnd; newEnd = safeEnd;
} }
this.setRange(newStart, newEnd, false, true, event);
var options = {
animation: false,
byUser: true,
event: event
}
this.setRange(newStart, newEnd, options);
this.startToFront = false; // revert to default this.startToFront = false; // revert to default
this.endToFront = true; // revert to default this.endToFront = true; // revert to default
@ -843,7 +874,12 @@ Range.prototype.moveTo = function(moveTo) {
var newStart = this.start - diff; var newStart = this.start - diff;
var newEnd = this.end - diff; var newEnd = this.end - diff;
this.setRange(newStart, newEnd, false, true, null);
var options = {
animation: false,
byUser: true,
event: null
}
this.setRange(newStart, newEnd, options);
}; };
module.exports = Range; module.exports = Range;

+ 36
- 37
lib/timeline/TimeStep.js View File

@ -560,6 +560,7 @@ TimeStep.prototype.getClassName = function() {
var m = this.moment(this.current); var m = this.moment(this.current);
var current = m.locale ? m.locale('en') : m.lang('en'); // old versions of moment have .lang() function var current = m.locale ? m.locale('en') : m.lang('en'); // old versions of moment have .lang() function
var step = this.step; var step = this.step;
var classNames = [];
function even(value) { function even(value) {
return (value / step % 2 == 0) ? ' vis-even' : ' vis-odd'; return (value / step % 2 == 0) ? ' vis-even' : ' vis-odd';
@ -592,51 +593,49 @@ TimeStep.prototype.getClassName = function() {
switch (this.scale) { switch (this.scale) {
case 'millisecond': case 'millisecond':
return today(current) +
even(current.milliseconds()).trim();
classNames.push(today(current));
classNames.push(even(current.milliseconds()));
break;
case 'second': case 'second':
return today(current) +
even(current.seconds()).trim();
classNames.push(today(current));
classNames.push(even(current.seconds()));
break;
case 'minute': case 'minute':
return today(current) +
even(current.minutes()).trim();
classNames.push(today(current));
classNames.push(even(current.minutes()));
break;
case 'hour': case 'hour':
return 'vis-h' + current.hours() +
(this.step == 4 ? '-h' + (current.hours() + 4) : '') +
today(current) +
even(current.hours());
classNames.push('vis-h' + current.hours() + this.step == 4 ? '-h' + (current.hours() + 4) : '');
classNames.push(today(current));
classNames.push(even(current.hours()));
break;
case 'weekday': case 'weekday':
return 'vis-' + current.format('dddd').toLowerCase() +
today(current) +
currentWeek(current) +
even(current.date());
classNames.push('vis-' + current.format('dddd').toLowerCase());
classNames.push(today(current));
classNames.push(currentWeek(current));
classNames.push(even(current.date()));
break;
case 'day': case 'day':
return 'vis-day' + current.date() +
' vis-' + current.format('MMMM').toLowerCase() +
today(current) +
currentMonth(current) +
(this.step <= 2 ? today(current) : '') +
(this.step <= 2 ? ' vis-' + current.format('dddd').toLowerCase() : '' + even(current.date() - 1));
classNames.push('vis-day' + current.date());
classNames.push('vis-' + current.format('MMMM').toLowerCase());
classNames.push(today(current));
classNames.push(currentMonth(current));
classNames.push(this.step <= 2 ? today(current) : '');
classNames.push(this.step <= 2 ? 'vis-' + current.format('dddd').toLowerCase() : '');
classNames.push(even(current.date() - 1));
break;
case 'month': case 'month':
return 'vis-' + current.format('MMMM').toLowerCase() +
currentMonth(current) +
even(current.month());
classNames.push('vis-' + current.format('MMMM').toLowerCase());
classNames.push(currentMonth(current));
classNames.push(even(current.month()));
break;
case 'year': case 'year':
var year = current.year();
return 'vis-year' + year +
currentYear(current) +
even(year);
default:
return '';
classNames.push('vis-year' + current.year());
classNames.push(currentYear(current));
classNames.push(even(current.year()));
break;
} }
return classNames.filter(String).join(" ");
}; };
module.exports = TimeStep; module.exports = TimeStep;

+ 2
- 2
lib/timeline/Timeline.js View File

@ -382,7 +382,7 @@ Timeline.prototype.focus = function(id, options) {
var interval = Math.max((this.range.end - this.range.start), (end - start) * 1.1); var interval = Math.max((this.range.end - this.range.start), (end - start) * 1.1);
var animation = (options && options.animation !== undefined) ? options.animation : true; var animation = (options && options.animation !== undefined) ? options.animation : true;
this.range.setRange(middle - interval / 2, middle + interval / 2, animation);
this.range.setRange(middle - interval / 2, middle + interval / 2, { animation: animation });
} }
}; };
@ -409,7 +409,7 @@ Timeline.prototype.fit = function (options) {
else { else {
// exactly fit the items (plus a small margin) // exactly fit the items (plus a small margin)
range = this.getItemRange(); range = this.getItemRange();
this.range.setRange(range.min, range.max, animation);
this.range.setRange(range.min, range.max, { animation: animation });
} }
}; };

+ 2
- 2
lib/timeline/component/CustomTime.js View File

@ -217,7 +217,7 @@ CustomTime.prototype._onDrag = function (event) {
this.body.emitter.emit('timechange', { this.body.emitter.emit('timechange', {
id: this.options.id, id: this.options.id,
time: new Date(this.customTime.valueOf()), time: new Date(this.customTime.valueOf()),
event: util.elementsCensor(event)
event: event
}); });
event.stopPropagation(); event.stopPropagation();
@ -235,7 +235,7 @@ CustomTime.prototype._onDragEnd = function (event) {
this.body.emitter.emit('timechanged', { this.body.emitter.emit('timechanged', {
id: this.options.id, id: this.options.id,
time: new Date(this.customTime.valueOf()), time: new Date(this.customTime.valueOf()),
event: util.elementsCensor(event)
event: event
}); });
event.stopPropagation(); event.stopPropagation();

+ 71
- 39
lib/timeline/component/Group.js View File

@ -133,21 +133,34 @@ Group.prototype.setData = function(data) {
} }
if (data && data.nestedGroups) { if (data && data.nestedGroups) {
if (data.showNested == false) {
this.showNested = false;
} else {
this.showNested = true;
if (!this.nestedGroups || this.nestedGroups != data.nestedGroups) {
this.nestedGroups = data.nestedGroups;
}
if (data.showNested !== undefined || this.showNested === undefined) {
if (data.showNested == false) {
this.showNested = false;
} else {
this.showNested = true;
}
} }
util.addClassName(this.dom.label, 'vis-nesting-group'); util.addClassName(this.dom.label, 'vis-nesting-group');
var collapsedDirClassName = this.itemSet.options.rtl ? 'collapsed-rtl' : 'collapsed'
if (this.showNested) { if (this.showNested) {
util.removeClassName(this.dom.label, 'collapsed');
util.removeClassName(this.dom.label, collapsedDirClassName);
util.addClassName(this.dom.label, 'expanded'); util.addClassName(this.dom.label, 'expanded');
} else { } else {
util.removeClassName(this.dom.label, 'expanded'); util.removeClassName(this.dom.label, 'expanded');
var collapsedDirClassName = this.itemSet.options.rtl ? 'collapsed-rtl' : 'collapsed'
util.addClassName(this.dom.label, collapsedDirClassName); util.addClassName(this.dom.label, collapsedDirClassName);
} }
} else if (this.nestedGroups) {
this.nestedGroups = null;
var collapsedDirClassName = this.itemSet.options.rtl ? 'collapsed-rtl' : 'collapsed'
util.removeClassName(this.dom.label, collapsedDirClassName);
util.removeClassName(this.dom.label, 'expanded');
util.removeClassName(this.dom.label, 'vis-nesting-group');
} }
if (data && data.nestedInGroup) { if (data && data.nestedInGroup) {
@ -425,30 +438,9 @@ Group.prototype.add = function(item) {
// add to // add to
if (item.data.subgroup !== undefined) { if (item.data.subgroup !== undefined) {
if (this.subgroups[item.data.subgroup] === undefined) {
this.subgroups[item.data.subgroup] = {
height:0,
top: 0,
start: item.data.start,
end: item.data.end,
visible: false,
index:this.subgroupIndex,
items: []
};
this.subgroupIndex++;
}
if (new Date(item.data.start) < new Date(this.subgroups[item.data.subgroup].start)) {
this.subgroups[item.data.subgroup].start = item.data.start;
}
if (new Date(item.data.end) > new Date(this.subgroups[item.data.subgroup].end)) {
this.subgroups[item.data.subgroup].end = item.data.end;
}
this.subgroups[item.data.subgroup].items.push(item);
this._addToSubgroup(item);
this.orderSubgroups();
} }
this.orderSubgroups();
if (this.visibleItems.indexOf(item) == -1) { if (this.visibleItems.indexOf(item) == -1) {
var range = this.itemSet.body.range; // TODO: not nice accessing the range like this var range = this.itemSet.body.range; // TODO: not nice accessing the range like this
@ -456,6 +448,34 @@ Group.prototype.add = function(item) {
} }
}; };
Group.prototype._addToSubgroup = function(item, subgroupId) {
subgroupId = subgroupId || item.data.subgroup;
if (subgroupId != undefined && this.subgroups[subgroupId] === undefined) {
this.subgroups[subgroupId] = {
height:0,
top: 0,
start: item.data.start,
end: item.data.end,
visible: false,
index:this.subgroupIndex,
items: []
};
this.subgroupIndex++;
}
if (new Date(item.data.start) < new Date(this.subgroups[subgroupId].start)) {
this.subgroups[subgroupId].start = item.data.start;
}
if (new Date(item.data.end) > new Date(this.subgroups[subgroupId].end)) {
this.subgroups[subgroupId].end = item.data.end;
}
this.subgroups[subgroupId].items.push(item);
};
Group.prototype._updateSubgroupsSizes = function () { Group.prototype._updateSubgroupsSizes = function () {
var me = this; var me = this;
if (me.subgroups) { if (me.subgroups) {
@ -526,22 +546,30 @@ Group.prototype.remove = function(item) {
if (index != -1) this.visibleItems.splice(index, 1); if (index != -1) this.visibleItems.splice(index, 1);
if(item.data.subgroup !== undefined){ if(item.data.subgroup !== undefined){
var subgroup = this.subgroups[item.data.subgroup];
this._removeFromSubgroup(item);
this.orderSubgroups();
}
};
Group.prototype._removeFromSubgroup = function(item, subgroupId) {
subgroupId = subgroupId || item.data.subgroup;
if (subgroupId != undefined) {
var subgroup = this.subgroups[subgroupId];
if (subgroup){ if (subgroup){
var itemIndex = subgroup.items.indexOf(item); var itemIndex = subgroup.items.indexOf(item);
subgroup.items.splice(itemIndex,1);
if (!subgroup.items.length){
delete this.subgroups[item.data.subgroup];
this.subgroupIndex--;
} else {
this._updateSubgroupsSizes();
// Check the item is actually in this subgroup. How should items not in the group be handled?
if (itemIndex >= 0) {
subgroup.items.splice(itemIndex,1);
if (!subgroup.items.length){
delete this.subgroups[subgroupId];
} else {
this._updateSubgroupsSizes();
}
} }
this.orderSubgroups();
} }
} }
}; };
/** /**
* Remove an item from the corresponding DataSet * Remove an item from the corresponding DataSet
* @param {Item} item * @param {Item} item
@ -722,6 +750,10 @@ Group.prototype._checkIfVisibleWithReference = function(item, visibleItems, visi
} }
}; };
Group.prototype.changeSubgroup = function(item, oldSubgroup, newSubgroup) {
this._removeFromSubgroup(item, oldSubgroup);
this._addToSubgroup(item, newSubgroup);
this.orderSubgroups();
};
module.exports = Group; module.exports = Group;

+ 16
- 9
lib/timeline/component/ItemSet.js View File

@ -546,7 +546,7 @@ ItemSet.prototype.getVisibleItems = function() {
for (var groupId in this.groups) { for (var groupId in this.groups) {
if (this.groups.hasOwnProperty(groupId)) { if (this.groups.hasOwnProperty(groupId)) {
var group = this.groups[groupId]; var group = this.groups[groupId];
var rawVisibleItems = group.visibleItems;
var rawVisibleItems = group.isVisible ? group.visibleItems : [];
// filter the "raw" set with visibleItems into a set which is really // filter the "raw" set with visibleItems into a set which is really
// visible by pixels // visible by pixels
@ -1199,21 +1199,28 @@ ItemSet.prototype._updateItem = function(item, itemData) {
var oldGroupId = item.data.group; var oldGroupId = item.data.group;
var oldSubGroupId = item.data.subgroup; 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) // update the items data (will redraw the item when displayed)
item.setData(itemData); item.setData(itemData);
var groupId = this._getGroupId(item.data); var groupId = this._getGroupId(item.data);
var group = this.groups[groupId];
var group = this.groups[groupId];
if (!group) { if (!group) {
item.groupShowing = false;
item.groupShowing = false;
} else if (group && group.data && group.data.showNested) { } else if (group && group.data && group.data.showNested) {
item.groupShowing = true;
item.groupShowing = true;
} }
// update group // update group
if (oldGroupId != item.data.group || oldSubGroupId != item.data.subgroup) {
var oldGroup = this.groups[oldGroupId];
if (oldGroup) oldGroup.remove(item);
if (group) group.add(item);
if (group) {
if (oldGroupId != item.data.group) {
group.add(item);
} else if (oldSubGroupId != item.data.subgroup) {
group.changeSubgroup(item, oldSubGroupId);
}
} }
}; };
@ -1644,7 +1651,7 @@ ItemSet.prototype._onDragEnd = function (event) {
ItemSet.prototype._onGroupClick = function (event) { ItemSet.prototype._onGroupClick = function (event) {
var group = this.groupFromTarget(event); var group = this.groupFromTarget(event);
if (!group.nestedGroups) return;
if (!group || !group.nestedGroups) return;
var groupsData = this.groupsData; var groupsData = this.groupsData;
if (this.groupsData instanceof DataView) { if (this.groupsData instanceof DataView) {

+ 5
- 1
lib/timeline/optionsTimeline.js View File

@ -26,7 +26,11 @@ let allOptions = {
//globals : //globals :
align: {string}, align: {string},
rtl: { 'boolean': bool, 'undefined': 'undefined'}, rtl: { 'boolean': bool, 'undefined': 'undefined'},
rollingMode: { 'boolean': bool, 'undefined': 'undefined'},
rollingMode: {
follow: { 'boolean': bool },
offset: {number,'undefined': 'undefined'},
__type__: {object}
},
verticalScroll: { 'boolean': bool, 'undefined': 'undefined'}, verticalScroll: { 'boolean': bool, 'undefined': 'undefined'},
horizontalScroll: { 'boolean': bool, 'undefined': 'undefined'}, horizontalScroll: { 'boolean': bool, 'undefined': 'undefined'},
autoResize: { 'boolean': bool}, autoResize: { 'boolean': bool},

+ 1
- 0
misc/we_need_help.md View File

@ -16,3 +16,4 @@ If you have shown some commitment to the project you can ask [@ludost](//github.
* [@Tooa](//github.com/Tooa) * [@Tooa](//github.com/Tooa)
* [@eymiha](//github.com/eymiha) * [@eymiha](//github.com/eymiha)
* [@bradh](//github.com/bradh) * [@bradh](//github.com/bradh)
* [@wimrijnders](//github.com/wimrijnders)

+ 11
- 11
package.json View File

@ -1,6 +1,6 @@
{ {
"name": "vis", "name": "vis",
"version": "4.18.1-SNAPSHOT",
"version": "4.19.1-SNAPSHOT",
"description": "A dynamic, browser-based visualization library.", "description": "A dynamic, browser-based visualization library.",
"homepage": "http://visjs.org/", "homepage": "http://visjs.org/",
"license": "(Apache-2.0 OR MIT)", "license": "(Apache-2.0 OR MIT)",
@ -28,25 +28,25 @@
"lint": "eslint lib", "lint": "eslint lib",
"watch": "gulp watch", "watch": "gulp watch",
"watch-dev": "gulp watch --bundle" "watch-dev": "gulp watch --bundle"
},
},
"dependencies": { "dependencies": {
"babel-core": "^6.6.5",
"babel-loader": "^6.2.4",
"babel-polyfill": "^6.22.0",
"babel-plugin-transform-es3-member-expression-literals": "^6.22.0",
"babel-plugin-transform-es3-property-literals": "^6.8.0",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-preset-es2015": "^6.6.0",
"babel-runtime": "^6.22.0",
"emitter-component": "^1.1.1", "emitter-component": "^1.1.1",
"moment": "^2.17.1",
"moment": "^2.18.1",
"propagating-hammerjs": "^1.4.6", "propagating-hammerjs": "^1.4.6",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"keycharm": "^0.2.0" "keycharm": "^0.2.0"
}, },
"devDependencies": { "devDependencies": {
"async": "^2.1.4", "async": "^2.1.4",
"babel-core": "^6.6.5",
"babel-eslint": "^7.1.1", "babel-eslint": "^7.1.1",
"babel-loader": "^6.2.4",
"babel-polyfill": "^6.22.0",
"babel-plugin-transform-es3-member-expression-literals": "^6.22.0",
"babel-plugin-transform-es3-property-literals": "^6.8.0",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-preset-es2015": "^6.6.0",
"babel-runtime": "^6.22.0",
"babelify": "^7.3.0", "babelify": "^7.3.0",
"clean-css": "^4.0.2", "clean-css": "^4.0.2",
"eslint": "^3.15.0", "eslint": "^3.15.0",

+ 44
- 0
test/TimeStep.test.js View File

@ -0,0 +1,44 @@
var assert = require('assert');
var vis = require('../dist/vis');
var jsdom = require('mocha-jsdom')
var moment = vis.moment;
var timeline = vis.timeline;
var TimeStep = timeline.TimeStep;
var TestSupport = require('./TestSupport');
describe('TimeStep', function () {
jsdom();
it('should work with just start and end dates', function () {
var timestep = new TimeStep(new Date(2017, 3, 3), new Date(2017, 3, 5));
assert.equal(timestep.autoScale, true, "should autoscale if scale not specified");
assert.equal(timestep.scale, "day", "should default to day scale if scale not specified");
assert.equal(timestep.step, 1, "should default to 1 day step if scale not specified");
});
it('should work with specified scale (just under 1 second)', function () {
var timestep = new TimeStep(new Date(2017, 3, 3), new Date(2017, 3, 5), 999);
assert.equal(timestep.scale, "second", "should have right scale");
assert.equal(timestep.step, 1, "should have right step size");
});
// TODO: check this - maybe should work for 1000?
it('should work with specified scale (1 second)', function () {
var timestep = new TimeStep(new Date(2017, 3, 3), new Date(2017, 3, 5), 1001);
assert.equal(timestep.scale, "second", "should have right scale");
assert.equal(timestep.step, 5, "should have right step size");
});
it('should work with specified scale (2 seconds)', function () {
var timestep = new TimeStep(new Date(2017, 3, 3), new Date(2017, 3, 5), 2000);
assert.equal(timestep.scale, "second", "should have right scale");
assert.equal(timestep.step, 5, "should have right step size");
});
it('should work with specified scale (5 seconds)', function () {
var timestep = new TimeStep(new Date(2017, 3, 3), new Date(2017, 3, 5), 5001);
assert.equal(timestep.scale, "second", "should have right scale");
assert.equal(timestep.step, 10, "should have right step size");
});
});

Loading…
Cancel
Save