Browse Source

Merge pull request #5 from almende/develop

Pull in changes from latest develop
codeClimate
Coleman Kane 8 years ago
parent
commit
6b398f5d2b
122 changed files with 10735 additions and 6504 deletions
  1. +233
    -1
      HISTORY.md
  2. +1
    -1
      LICENSE-MIT
  3. +1
    -1
      NOTICE
  4. +38
    -25
      README.md
  5. +1
    -2
      bower.json
  6. +29
    -15
      dist/vis.css
  7. +5959
    -3947
      dist/vis.js
  8. +1
    -1
      dist/vis.map
  9. +1
    -1
      dist/vis.min.css
  10. +23
    -22
      dist/vis.min.js
  11. +6
    -6
      docs/data/dataset.html
  12. +1
    -1
      docs/data/dataview.html
  13. +57
    -41
      docs/graph2d/index.html
  14. +7
    -0
      docs/graph3d/index.html
  15. +14
    -0
      docs/network/edges.html
  16. +37
    -19
      docs/network/index.html
  17. +14
    -2
      docs/network/layout.html
  18. +31
    -5
      docs/network/nodes.html
  19. +59
    -3
      docs/timeline/index.html
  20. +1
    -1
      examples/graph2d/06_interpolation.html
  21. +14
    -13
      examples/graph2d/17_dynamicStyling.html
  22. +117
    -0
      examples/graph2d/20_shading.html
  23. +2
    -0
      examples/network/datasources/largeHierarchicalDataset.js
  24. +63
    -0
      examples/network/exampleUtil.js
  25. +1
    -1
      examples/network/layout/hierarchicalLayout.html
  26. +1
    -1
      examples/network/layout/hierarchicalLayoutMethods.html
  27. +107
    -102
      examples/network/layout/hierarchicalLayoutUserdefined.html
  28. +89
    -0
      examples/network/layout/hierarchicalLayoutWithoutPhysics.html
  29. +3
    -5
      examples/network/nodeStyles/customGroups.html
  30. +105
    -0
      examples/network/nodeStyles/imagesWithBorders.html
  31. +1
    -1
      examples/network/nodeStyles/shadows.html
  32. +4
    -4
      examples/network/other/clustering.html
  33. +5
    -1
      examples/network/other/clusteringByZoom.html
  34. +11
    -0
      examples/network/other/configuration.html
  35. +4
    -4
      gulpfile.js
  36. +3
    -2
      index.js
  37. +12
    -2
      lib/DOMutil.js
  38. +158
    -125
      lib/DataSet.js
  39. +55
    -17
      lib/DataView.js
  40. +9
    -5
      lib/graph3d/Graph3d.js
  41. +19
    -21
      lib/hammerUtil.js
  42. +1
    -1
      lib/header.js
  43. +7
    -5
      lib/network/Network.js
  44. +96
    -0
      lib/network/NetworkUtil.js
  45. +8
    -5
      lib/network/css/network-colorpicker.css
  46. +9
    -8
      lib/network/css/network-manipulation.css
  47. +1
    -1
      lib/network/css/network-tooltip.css
  48. +60
    -13
      lib/network/modules/Canvas.js
  49. +12
    -31
      lib/network/modules/CanvasRenderer.js
  50. +86
    -89
      lib/network/modules/Clustering.js
  51. +8
    -2
      lib/network/modules/EdgesHandler.js
  52. +49
    -44
      lib/network/modules/InteractionHandler.js
  53. +0
    -4
      lib/network/modules/KamadaKawai.js
  54. +937
    -144
      lib/network/modules/LayoutEngine.js
  55. +24
    -23
      lib/network/modules/ManipulationSystem.js
  56. +11
    -4
      lib/network/modules/NodesHandler.js
  57. +60
    -17
      lib/network/modules/PhysicsEngine.js
  58. +49
    -39
      lib/network/modules/SelectionHandler.js
  59. +31
    -84
      lib/network/modules/View.js
  60. +59
    -26
      lib/network/modules/components/Edge.js
  61. +18
    -13
      lib/network/modules/components/Node.js
  62. +5
    -2
      lib/network/modules/components/algorithms/FloydWarshall.js
  63. +30
    -10
      lib/network/modules/components/edges/BezierEdgeDynamic.js
  64. +21
    -15
      lib/network/modules/components/edges/BezierEdgeStatic.js
  65. +14
    -12
      lib/network/modules/components/edges/CubicBezierEdge.js
  66. +7
    -4
      lib/network/modules/components/edges/StraightEdge.js
  67. +55
    -54
      lib/network/modules/components/edges/util/EdgeBase.js
  68. +21
    -16
      lib/network/modules/components/nodes/shapes/Box.js
  69. +1
    -5
      lib/network/modules/components/nodes/shapes/Circle.js
  70. +3
    -3
      lib/network/modules/components/nodes/shapes/CircularImage.js
  71. +12
    -15
      lib/network/modules/components/nodes/shapes/Database.js
  72. +1
    -1
      lib/network/modules/components/nodes/shapes/Diamond.js
  73. +2
    -1
      lib/network/modules/components/nodes/shapes/Dot.js
  74. +13
    -10
      lib/network/modules/components/nodes/shapes/Ellipse.js
  75. +1
    -2
      lib/network/modules/components/nodes/shapes/Icon.js
  76. +37
    -6
      lib/network/modules/components/nodes/shapes/Image.js
  77. +1
    -2
      lib/network/modules/components/nodes/shapes/Square.js
  78. +1
    -1
      lib/network/modules/components/nodes/shapes/Star.js
  79. +1
    -2
      lib/network/modules/components/nodes/shapes/Text.js
  80. +1
    -1
      lib/network/modules/components/nodes/shapes/Triangle.js
  81. +1
    -1
      lib/network/modules/components/nodes/shapes/TriangleDown.js
  82. +42
    -12
      lib/network/modules/components/nodes/util/CircleImageBase.js
  83. +4
    -3
      lib/network/modules/components/nodes/util/NodeBase.js
  84. +11
    -9
      lib/network/modules/components/nodes/util/ShapeBase.js
  85. +5
    -2
      lib/network/modules/components/physics/BarnesHutSolver.js
  86. +19
    -0
      lib/network/options.js
  87. +34
    -10
      lib/shared/ColorPicker.js
  88. +17
    -10
      lib/shared/Configurator.js
  89. +6
    -1
      lib/shared/configuration.css
  90. +55
    -15
      lib/timeline/Core.js
  91. +0
    -233
      lib/timeline/DataStep.js
  92. +11
    -3
      lib/timeline/DateUtil.js
  93. +7
    -6
      lib/timeline/Graph2d.js
  94. +7
    -3
      lib/timeline/Range.js
  95. +7
    -2
      lib/timeline/TimeStep.js
  96. +31
    -26
      lib/timeline/Timeline.js
  97. +2
    -1
      lib/timeline/component/CurrentTime.js
  98. +19
    -6
      lib/timeline/component/CustomTime.js
  99. +55
    -105
      lib/timeline/component/DataAxis.js
  100. +237
    -0
      lib/timeline/component/DataScale.js

+ 233
- 1
HISTORY.md View File

@ -2,11 +2,243 @@
http://visjs.org
## not yet released, version 4.8.1-SNAPSHOT
## not-yet-released, version 4.15.1
### Graph2d
- Fixed #1692: Error when y-axis values are equal.
### Timeline
- Fixed #1695: Item line and dot not correctly reckoning with the line width
when using left or right align.
## 2016-02-23, version 4.15.0
### Timeline
- Implemented `currentTimeTick` event (see #1683).
- Fixed #1630: method `getItemRange` missing in docs.
- Fixed #1455: allow vertical panning of the web page on touch devices.
### Graph2d
- Fixed #1630: method `getDataRange` was wrongly called `getItemRange` in docs.
- Fixed #1655: use parseFloat instead of Number.parseFloat, as the latter is
not supported in IE. Thanks @ttjoseph.
### Graph3d
- Changed the built-in tooltip to show the provided `xLabel`, `yLabel`, and
`zLabel` instead of `'x'`, `'y'`, and `'z'`. Thanks @jacklightbody.
### Network
- Implemented interpolation option for interpolation of images, default true.
- Implemented parentCentralization option for hierarchical layout.
- Fixed #1635: edges are now referring to the correct points.
- Fixed #1644, #1631: overlapping nodes in hierarchical layout should no longer occur.
- Fixed #1575: fixed selection events
- Fixed #1677: updating groups through manipulation now works as it should.
- Fixed #1672: Implemented stepped scaling for nice interpolation of images.
## 2016-02-04, version 4.14.0
### Timeline
- Fixed a regression: Timeline/Graph2d constructor throwing an exception when
no options are provided via the constructor.
### Graph2d
- Fixed a regression: Timeline/Graph2d constructor throwing an exception when
no options are provided via the constructor.
### Graph3d
- Fixed #1615: implemented new option `dotSizeRatio`.
## 2016-02-01, version 4.13.0
### Network
- Added options to customize the hierarchical layout without the use of physics.
- Altered edges for arrows and added the arrowStrikethrough option.
- Improved the hierarchical layout algorithm by adding a condensing method to remove whitespace.
- Fixed #1556: Network throwing an error when clicking the "Edit" button
on the manipulation toolbar.
- Fixed #1334 (again): Network now ignores scroll when interaction:zoomView is false.
- Fixed #1588: destroy now unsubscribed from the dataset.
- Fixed #1584: Navigation buttons broken.
- Fixed #1596: correct clean up of manipulation dom elements.
- Fixed #1594: bug in hierarchical layout.
- Fixed #1597: Allow zero borders and addressed scaling artifacts.
- Fixed #1608: Fixed wrong variable reference
### Timeline
- Moved initial autoscale/fit method to an handler of the "changed" event.
- Fixed #1580: Invisible timeline/graph should not be drawn, as most inputs are invalid
- Fixed #1521: Prevent items from staying stuck to the left side of the viewport.
- Fixed #1592: Emit a "changed" event after each redraw.
- Fixed #1541: Timeline and Graph2d did not load synchronously anymore.
### Graph2d
- Major redesign of data axis/scales, with large focus on creating a sane slave axis setup
- Cleanup of linegraph's event handling.
- Fixed #1585: Allow bar groups to exclude from stacking
- Fixed #1580: Invisible timeline/graph should not be drawn, as most inputs are invalid
- Fixed #1177: Fix custom range of slaved right axis.
- Fixed #1592: Emit a "changed" event after each redraw.
- Fixed #1017: Fixed minWidth behavior for bars.
- Fixes #1557: Fix default axis formatting function.
- Fixed #1541: Timeline and Graph2d did not load synchronously anymore.
- Fixed a performance regression
## 2016-01-08, version 4.12.0
### Timeline
- Fixed #1527: error when creating/updating a Timeline without data.
- Fixed #1127: `doubleClick` event not being fired.
- Fixed #1554: wrong cursor on readonly range items.
### Network
- Fixed #1531, #1335: border distances for arrow positioning
- Fixed findNode method. It now does not return internal objects anymore.
- Fixed #1529, clustering and declustering now respects the original settings of the edges for physics and hidden.
- Fixed #1406, control nodes are now drawn immediately without a second redraw.
- Fixed #1404, made the array returned by findNode match the docs.
- Added #1138, enable the user to define the color of the shadows for nodes and edges.
- Fixed #1528, #1278, avoided ID's being cast to string for methods that return ID's as well as storePositions casting to string.
- Fixed upscaling when the window size increases.
- Accepted pull request #1544, thanks @felixhayashi!
- Fixed documented bug in #1544.
## 2015-12-18, version 4.11.0
### Network
- Expose `setSelection` method. Thanks @zefrog.
### Timeline
- Fixed #1441: Height of subgroups not immediately updated after updating
data in a DataSet or DataView.
- Fixed #1491: Problem using ctrl+drag in combination with using a `DataView`,
and an issue with ctrl+drag when using `snap: null`.
- Fixed #1486: Item range sometimes wrongly calculated on IE in case of old dates.
- Fixed #1523: end of data range wrongly determined.
### Graph2d
- Large refactoring of Graph2d code base:
- Implemented a new option for `shaded.orientation` to always shade towards zero.
- Implemented a new option for `shaded.orientation` to follow another group (fill in between)
- Implemented line-graph stacking
- Fixed support for using a `DataView` in Graph2d.
- Implemented a new zindex option for controlling svg rendering order.
- Performance updates and fixes
### DataSet
- Fixed #1487: DataSet cannot remove an item with id `0` correctly.
### DataView
- Added the map() function from DataSet.
## 2015-11-27, version 4.10.0
### General
- Fixed #1353: Custom bundling with browserify requiring manual installation
of `babelify`.
### Network
- Implemented new method `setSelection({nodes:[...], edges: [...]})`.
Thanks @zefrog.
- Fixed #1343: Connected edges are now deselected too when deselecting a node.
- Fixed #1398: Support nodes start with the correct positions.
- Fixed #1324: Labels now scale again.
- Fixed #1362: Layout of hierarchicaly systems no longer overlaps NODES.
- Fixed #1414: Fixed color references for nodes and edges.
- Fixed #1408: Unclustering without release function respects fixed positions now.
- Fixed #1358: Fixed example for clustering on zoom.
- Fixed #1416: Fixed error in improvedLayout.
- Improvements on hierarchical layout.
### Timeline
- Implemented option `itemsAlwaysDraggable`, See #1395. Thanks @liuqingc.
- Implemented option `multiselectPerGroup`. Thanks @hansmaulwurf23.
- Implemented property `oldData` on change events of the DataSet, and
deprecated the `data` property which wrongly contained new data instead of
old data. Thanks @hansmaulwurf23.
- Implemented option `maxMinorChars` to customize the width of the grid.
- Expose `vis.timeline.Core` for customization purposes.
- Fixed #1449, #1393: text of minor grids sometimes not being drawn.
### Graph2d
- Fixed #1385: Draw lines on top of bars.
- Fixed #1461 and #1345: Reset order of SVG elements in legend icons.
### DataSet/DataView
- Performance improvements (see #1381). Thanks @phimimms.
## 2015-10-01, version 4.9.0
### Network
- Fixed bug where an edge that was not connected would crash the layout algorithms.
- Fixed bug where a box shape could not be drawn outside of the viewable area.
- Fixed bug where dragging a node that is not a control node during edit edge mode would throw an error.
- Made auto scaling on container size change pick the lowest between delta height and delta width.
- Added images with borders option (useBorderWithImage)
- Updated the manipulation css to fix offset if there is no separator.
### Timeline
- Fixed #1326: wrongly positioned dot of PointItems.
- Fixed #1249: option `hiddenDates` not accepting a single hidden date.
- Fixed a bug when pinching and using hidden dates. Thanks @lauzierj.
## 2015-09-14, version 4.8.2
### Network
- Fixed Phantom Edges during clustering.
- Fixed scaling not doing anything to edges.
- Fixed setting font to null so the network won't crash anymore.
- Fixed stabilized event not firing if layout algorithm does very well.
- Fixed arrows with some shapes when they are selected. #1292
- Fixed deletion of options by settings them to null.
## 2015-09-07, version 4.8.1
### Network
- Added German (de) locale. Thanks @Tooa.
- Fixed critical camera zoom bug #1273.
- Fixed unselectAll method. #1256
- Fixed bug that broke the network if drawn in a hidden div #1254
### Timeline
- Fixed #1215: inconsistent types of properties `start` and `end` in callback
functions `onMove`, `onMoving`, `onAdd`.
## 2015-08-28, version 4.8.0

+ 1
- 1
LICENSE-MIT View File

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2014-2015 Almende B.V.
Copyright (c) 2014-2016 Almende B.V.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

+ 1
- 1
NOTICE View File

@ -1,5 +1,5 @@
Vis.js
Copyright 2010-2015 Almende B.V.
Copyright 2010-2016 Almende B.V.
Vis.js is dual licensed under both

+ 38
- 25
README.md View File

@ -22,15 +22,13 @@ The vis.js library is developed by [Almende B.V](http://almende.com).
Install via npm:
npm install vis
$ npm install vis
Install via bower:
bower install vis
$ bower install vis
Link via cdnjs:
http://cdnjs.com
Link via cdnjs: http://cdnjs.com
Or download the library from the github project:
[https://github.com/almende/vis.git](https://github.com/almende/vis.git).
@ -126,28 +124,28 @@ of the project.
To build the library from source, clone the project from github
git clone git://github.com/almende/vis.git
$ git clone git://github.com/almende/vis.git
The source code uses the module style of node (require and module.exports) to
organize dependencies. To install all dependencies and build the library,
run `npm install` in the root of the project.
cd vis
npm install
$ cd vis
$ npm install
Then, the project can be build running:
npm run build
$ npm run build
To automatically rebuild on changes in the source files, once can use
npm run watch
$ npm run watch
This will both build and minify the library on changes. Minifying is relatively
slow, so when only the non-minified library is needed, one can use the
`watch-dev` script instead:
npm run watch-dev
$ npm run watch-dev
## Custom builds
@ -163,10 +161,25 @@ The source code of vis.js consists of commonjs modules, which makes it possible
Before you can do a build:
- Install node.js, npm, browserify, and uglify-js on your system.
- Download or clone the vis.js project.
- Install the dependencies of vis.js by running `npm install` in the root of the project.
- Install node.js and npm on your system: https://nodejs.org/
- Install the following modules using npm: `browserify`, `babelify`, and `uglify-js`:
```
$ [sudo] npm install -g browserify babelify uglify-js
```
- Download or clone the vis.js project:
```
$ git clone https://github.com/almende/vis.git
```
- Install the dependencies of vis.js by running `npm install` in the root of the project:
```
$ cd vis
$ npm install
```
#### Example 1: Bundle a single visualization
@ -177,13 +190,13 @@ exports.DataSet = require('./lib/DataSet');
exports.Timeline = require('./lib/timeline/Timeline');
```
Install browserify globally via `[sudo] npm install -g browserify`, then create a custom bundle like:
Then create a custom bundle using browserify, like:
browserify custom.js -t babelify -o vis-custom.js -s vis
$ browserify custom.js -t babelify -o vis-custom.js -s vis
This will generate a custom bundle *vis-custom.js*, which exposes the namespace `vis` containing only `DataSet` and `Timeline`. The generated bundle can be minified with uglifyjs (installed globally with `[sudo] npm install -g uglify-js`):
This will generate a custom bundle *vis-custom.js*, which exposes the namespace `vis` containing only `DataSet` and `Timeline`. The generated bundle can be minified using uglifyjs:
uglifyjs vis-custom.js -o vis-custom.min.js
$ uglifyjs vis-custom.js -o vis-custom.min.js
The custom bundle can now be loaded like:
@ -204,11 +217,11 @@ The custom bundle can now be loaded like:
The default bundle `vis.js` is standalone and includes external dependencies such as hammer.js and moment.js. When these libraries are already loaded by the application, vis.js does not need to include these dependencies itself too. To build a custom bundle of vis.js excluding moment.js and hammer.js, run browserify in the root of the project:
browserify index.js -t babelify -o vis-custom.js -s vis -x moment -x hammerjs
$ browserify index.js -t babelify -o vis-custom.js -s vis -x moment -x hammerjs
This will generate a custom bundle *vis-custom.js*, which exposes the namespace `vis`, and has moment and hammerjs excluded. The generated bundle can be minified with uglifyjs:
uglifyjs vis-custom.js -o vis-custom.min.js
$ uglifyjs vis-custom.js -o vis-custom.min.js
The custom bundle can now be loaded as:
@ -254,12 +267,12 @@ var timeline = new Timeline(container, data, options);
Install the application dependencies via npm:
npm install vis moment
$ npm install vis moment
The application can be bundled and minified:
browserify app.js -o app-bundle.js -t babelify
uglifyjs app-bundle.js -o app-bundle.min.js
$ browserify app.js -o app-bundle.js -t babelify
$ uglifyjs app-bundle.js -o app-bundle.min.js
And loaded into a webpage:
@ -282,11 +295,11 @@ And loaded into a webpage:
To test the library, install the project dependencies once:
npm install
$ npm install
Then run the tests:
npm test
$ npm test
## License

+ 1
- 2
bower.json View File

@ -1,7 +1,6 @@
{
"name": "vis",
"version": "4.8.1-SNAPSHOT",
"main": ["dist/vis.min.js", "dist/vis.min.css"],
"main": ["dist/vis.js", "dist/vis.css"],
"description": "A dynamic, browser-based visualization library.",
"homepage": "http://visjs.org/",
"license": ["Apache-2.0", "MIT"],

+ 29
- 15
dist/vis.css View File

@ -32,6 +32,11 @@ div.vis-configuration-wrapper {
width:700px;
}
div.vis-configuration-wrapper::after {
clear: both;
content: "";
display: block;
}
div.vis-configuration.vis-config-option-container{
display:block;
@ -133,7 +138,7 @@ input.vis-configuration.vis-config-rangeinput{
position:relative;
top:-5px;
width:60px;
height:13px;
/*height:13px;*/
padding:1px;
margin:0;
pointer-events:none;
@ -584,6 +589,11 @@ input.vis-configuration.vis-config-range:focus::-ms-fill-upper {
cursor: e-resize;
}
.vis-range.vis-item.vis-readonly .vis-drag-left,
.vis-range.vis-item.vis-readonly .vis-drag-right {
cursor: auto;
}
.vis-time-axis {
position: relative;
overflow: hidden;
@ -903,17 +913,18 @@ div.vis-network div.vis-manipulation {
background: linear-gradient(to bottom, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* W3C */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#fcfcfc',GradientType=0 ); /* IE6-9 */
padding-top:4px;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 30px;
height: 28px;
}
div.vis-network div.vis-edit-mode {
position:absolute;
left: 0;
top: 15px;
top: 5px;
height: 30px;
}
@ -944,8 +955,7 @@ div.vis-network div.vis-close:hover {
div.vis-network div.vis-manipulation div.vis-button,
div.vis-network div.vis-edit-mode div.vis-button {
position:relative;
top:-7px;
float:left;
font-family: verdana;
font-size: 12px;
-moz-border-radius: 15px;
@ -954,8 +964,8 @@ div.vis-network div.vis-edit-mode div.vis-button {
background-position: 0px 0px;
background-repeat:no-repeat;
height:24px;
margin: 0px 0px 0px 10px;
vertical-align:middle;
margin-left: 10px;
/*vertical-align:middle;*/
cursor: pointer;
padding: 0px 8px 0px 8px;
-webkit-touch-callout: none;
@ -1021,11 +1031,12 @@ div.vis-network div.vis-edit-mode div.vis-label {
line-height: 25px;
}
div.vis-network div.vis-manipulation div.vis-separator-line {
float:left;
display:inline-block;
width:1px;
height:20px;
height:21px;
background-color: #bdbdbd;
margin: 5px 7px 0 15px;
margin: 0px 7px 0 15px; /*top right bottom left*/
}
/* TODO: is this redundant?
@ -1045,7 +1056,7 @@ div.vis-network-tooltip {
font-family: verdana;
font-size:14px;
font-color:#000000;
color:#000000;
background-color: #f5f4ed;
-moz-border-radius: 3px;
@ -1120,14 +1131,17 @@ div.vis-network div.vis-navigation div.vis-button.vis-zoomExtends {
div.vis-color-picker {
position:absolute;
top: 0px;
left: 30px;
margin-top:-140px;
margin-left:30px;
width:293px;
height:425px;
width:310px;
height:444px;
z-index: 1;
padding: 10px;
border-radius:15px;
background-color:#ffffff;
display:none;
display: none;
box-shadow: rgba(0,0,0,0.5) 0px 0px 10px 0px;
}
@ -1137,8 +1151,8 @@ div.vis-color-picker div.vis-arrow {
left:5px;
}
div.vis-color-picker div.vis-arrow:after,
div.vis-color-picker div.vis-arrow:before {
div.vis-color-picker div.vis-arrow::after,
div.vis-color-picker div.vis-arrow::before {
right: 100%;
top: 50%;
border: solid transparent;

+ 5959
- 3947
dist/vis.js
File diff suppressed because it is too large
View File


+ 1
- 1
dist/vis.map
File diff suppressed because it is too large
View File


+ 1
- 1
dist/vis.min.css
File diff suppressed because it is too large
View File


+ 23
- 22
dist/vis.min.js
File diff suppressed because it is too large
View File


+ 6
- 6
docs/data/dataset.html View File

@ -104,7 +104,7 @@
Vis.js comes with a flexible DataSet, which can be used to hold and
manipulate unstructured data and listen for changes in the data.
The DataSet is key/value based. Data items can be added, updated and
removed from the DatSet, and one can subscribe to changes in the DataSet.
removed from the DataSet, and one can subscribe to changes in the DataSet.
The data in the DataSet can be filtered and ordered, and fields (like
dates) can be converted to a specific type. Data can be normalized when
appending it to the DataSet as well.
@ -312,7 +312,7 @@ var data = new vis.DataSet([data] [, options])
<td>Object | Array</td>
<td>
Get a single item, multiple items, or all items from the DataSet.
Usage examples can be found in section <a href="#Getting_Data">Getting Data</a>, and the available <code>options</code> are described in section <a href="#Data_Selection">Data Selection</a>.
Usage examples can be found in section <a href="#Getting_Data">Getting Data</a>, and the available <code>options</code> are described in section <a href="#Data_Selection">Data Selection</a>. When no item is found, <code>null</code> is returned when a single item was requested, and and empty Array is returned in case of multiple id's.
</td>
</tr>
@ -598,9 +598,9 @@ function (event, properties, senderId) {
<code>update</code>, and <code>remove</code>,
<code>properties</code> is always an object containing a property
<code>items</code>, which contains an array with the ids of the affected
items. The <code>update</code> event has an extra field <code>data</code>
containing the original data of the updated items, i.e. the gives the
changed fields of the changed items.
items. The <code>update</code> event has an extra field <code>oldData</code>
containing the original data of the updated items, and a field <code>data</code>
containing the changes: the properties of the items that are being updated.
</td>
</tr>
<tr>
@ -1020,4 +1020,4 @@ var positiveBalance = dataset.get({
<script src="../js/tipuesearch.config.js"></script>
<script src="../js/tipuesearch.js"></script>
<!-- controller -->
<script src="../js/main.js"></script>
<script src="../js/main.js"></script>

+ 1
- 1
docs/data/dataview.html View File

@ -248,7 +248,7 @@ var data = new vis.DataView(dataset, options)
<td>Object | Array</td>
<td>
Get a single item, multiple items, or all items from the DataView.
Usage examples can be found in section <a href="#Getting_Data">Getting Data</a>, and the available <code>options</code> are described in section <a href="#Data_Selection">Data Selection</a>.
Usage examples can be found in section <a href="#Getting_Data">Getting Data</a>, and the available <code>options</code> are described in section <a href="#Data_Selection">Data Selection</a>. When no item is found, <code>null</code> is returned when a single item was requested, and and empty Array is returned in case of multiple id's.
</td>
</tr>

+ 57
- 41
docs/graph2d/index.html View File

@ -246,8 +246,6 @@
<p>
Graph2d can load data from an <code>Array</code>, a <code>DataSet</code> (offering 2 way data binding), or a <code>DataView</code> (offering one way data binding).
Objects are added to this DataSet by using the <code>add()</code> function.
Data points must have properties <code>x</code>, <code>y</code>, and <code>z</code>,
and can optionally have a property <code>style</code> and <code>filter</code>.
<p>
Graph2d can be provided with two types of data:
</p>
@ -436,7 +434,7 @@ var options = {
<td class="greenField indent">barChart.sideBySide</td>
<td>Boolean</td>
<td>false</td>
<td>If two datapoints of a barchart overlap, they are drawn over eachother by default. If sideBySide is set to true, they will be drawn side by side.
<td>If two datapoints of a barchart overlap, they are drawn over eachother by default. If sideBySide is set to true, they will be drawn side by side, within the same width as a single bar..
See <a href="../../examples/graph2d/10_barsSideBySide.html">example 10</a> for more information.
When using groups, see <a href="../../examples/graph2d/11_barsSideBySideGroups.html">example 11</a>.
</td>
@ -447,7 +445,12 @@ var options = {
<td>50</td>
<td>The width of the bars.</td>
</tr>
<tr parent="barChart" class="hidden">
<td class="greenField indent">barChart.minWidth</td>
<td>Number</td>
<td></td>
<td>The minimum width of the bars in pixels: by default the bars get smaller while zooming out to prevent overlap, this value is the minimum width of the bar. Default behavior (when minWidth is not set) is 10% of the bar width.</td>
</tr>
<tr class='toggle collapsible' onclick="toggleTable('g2dOptions','dataAxis', this);">
<td><span parent="dataAxis" class="right-caret"></span> dataAxis</td>
<td>Object</td>
@ -472,12 +475,11 @@ var options = {
<td>Function</td>
<td></td>
<td>Insert a custom function on how to format the label. The function will receive a numeric value and has to return a string. Default function is:
<pre class="code">
<pre class="prettyprint lang-js">
function (value) {
return value;
}
</pre>
which does nothing to it.</td>
return ''+value.toPrecision(3);
}</pre>
</td>
</tr>
<tr parent="dataAxis" class="hidden">
<td class="indent2">dataAxis.left.range.min</td>
@ -578,7 +580,7 @@ drawPoints: {
3. <code>Function</code>: If a function is provided it will be used as a callback. The function may return values from listing 1 or 2.<br />
<pre class="prettyprint lang-js">
drawPoints: function(item, group, graph2d) {
drawPoints: function(item, group) {
...
}
</pre>
@ -743,7 +745,7 @@ onRender: function(item, group, graph2d) {
<td>stack</td>
<td>Boolean</td>
<td>true</td>
<td>If stack is enabled, the graphs will be stacked upon eachother when applicable. This currently only works with bar graphs but linegraph support is being worked on.</td>
<td>If stack is enabled, the graphs will be stacked upon each-other when applicable. A group can opt-out of stacking through the "excludeFromStacking" option.</td>
</tr>
<tr class='toggle collapsible' onclick="toggleTable('g2dOptions','shaded', this);">
<td class="greenField"><span parent="shaded" class="right-caret"></span> shaded</td>
@ -761,7 +763,17 @@ onRender: function(item, group, graph2d) {
<td class="greenField indent">shaded.orientation</td>
<td>String</td>
<td>'bottom'</td>
<td>This determines if the shaded area is at the bottom or at the top of the curve. The options are 'bottom' or 'top'.</td>
<td>This determines if the shaded area is at the bottom or at the top of the curve, or always towards the zero-axis of the graph.
The options are 'zero', 'bottom', 'top', or the special case of 'group'. If group is chosen, the option groupId is required.
See <a href="../../examples/graph2d/20_shading.html">Example 20</a> what these options look like.
</td>
</tr>
<tr parent="shaded" class="hidden">
<td class="greenField indent">shaded.groupId</td>
<td>String</td>
<td>undefined</td>
<td>The id of the group which should be used as the other shading limit.
</td>
</tr>
<tr parent="shaded" class="hidden">
<td class="greenField indent">shaded.style</td>
@ -786,7 +798,13 @@ onRender: function(item, group, graph2d) {
<td class="greenField">excludeFromLegend</td>
<td>Boolean</td>
<td>false</td>
<td>Group option only. Excludes the the group from beeing listed in the legend.</td>
<td>Group option only. Excludes the group from being listed in the legend.</td>
</tr>
<tr>
<td class="greenField">excludeFromStacking</td>
<td>Boolean</td>
<td>false</td>
<td>Group option only. Excludes the group from being included in the stacking.</td>
</tr>
</table>
@ -927,6 +945,16 @@ function (option, path) {
<td>Specifies the maximum height for the Timeline. Can be a number in pixels or a string like "300px".</td>
</tr>
<tr>
<td>maxMinorChars</td>
<td>number</td>
<td>7</td>
<td>
Specifies the maximum number of characters that should fit in minor grid labels.
If larger, less and wider grids will be drawn.
</td>
</tr>
<tr>
<td>min</td>
<td>Date or Number or String</td>
@ -997,22 +1025,6 @@ function (option, path) {
visible.</td>
</tr>
<tr>
<td>showMajorLines</td>
<td>Boolean</td>
<td>true</td>
<td>By default, the timeline shows both minor and major date lines on the
time axis. You can use this option to hide the lines from the major dates.
</tr>
<tr>
<td>showMinorLines</td>
<td>Boolean</td>
<td>true</td>
<td>By default, the timeline shows both minor and major date lines on the
time axis. You can use this option to hide the lines from the minor dates.
</tr>
<tr>
<td>start</td>
<td>Date or Number or String</td>
@ -1133,6 +1145,12 @@ function (option, path) {
</td>
</tr>
<tr>
<td>getDataRange()</td>
<td>Object</td>
<td>Get the range of all the items as an object containing <code>min: Date</code> and <code>max: Date</code>.</td>
</tr>
<tr id="getEventProperties">
<td>getEventProperties(event)</td>
<td>Object</td>
@ -1164,13 +1182,6 @@ function (option, path) {
<td>Get the current visible window. Returns an object with properties <code>start: Date</code> and <code>end: Date</code>.</td>
</tr>
<tr>
<td>getItemRange()</td>
<td>Object</td>
<td>Get the range of all the items as an object containing <code>min: Date</code> and <code>max: Date</code>.</td>
</tr>
<tr>
<td>isGroupVisible(groupId)</td>
<td>Boolean</td>
@ -1293,6 +1304,12 @@ Graph2d.off('rangechanged', onChange);
<th>Properties</th>
<th>Description</th>
</tr>
<tr>
<td>currentTimeTick</td>
<td>Fired when the current time bar redraws. The rate depends on the zoom level.</td>
</tr>
<tr>
<td>click</td>
<td>
@ -1323,16 +1340,14 @@ Graph2d.off('rangechanged', onChange);
<td>Fired when double clicked inside the Graph2d.
</td>
</tr>
<tr>
<td>finishedRedraw</td>
<td>changed</td>
<td>
none.
Has no properties.
</td>
<td>Fired after a redraw is complete. When moving the Graph2d around, this could be fired frequently.
<td>Fired once after each graph redraw.
</td>
</tr>
<tr>
<td>rangechange</td>
<td>
@ -1356,6 +1371,7 @@ Graph2d.off('rangechanged', onChange);
<td>Fired once after the user has dragged the Graph2d window.
</td>
</tr>
<tr>
<td>timechange</td>
<td>

+ 7
- 0
docs/graph3d/index.html View File

@ -379,6 +379,13 @@ var options = {
<td>The line width of dots, bars and lines. Applicable for all styles.</td>
</tr>
<tr>
<td>dotSizeRatio</td>
<td>number</td>
<td>0.02</td>
<td>Ratio of the size of the dots with respect to the width of the graph.</td>
</tr>
<tr>
<td>gridColor</td>
<td>string</td>

+ 14
- 0
docs/network/edges.html View File

@ -119,6 +119,7 @@ var options = {
middle: {enabled: false, scaleFactor:1},
from: {enabled: false, scaleFactor:1}
},
arrowStrikethrough: true,
color: {
color:'#848484',
highlight:'#848484',
@ -166,6 +167,7 @@ var options = {
selfReferenceSize:20,
shadow:{
enabled: false,
color: 'rgba(0,0,0,0.5)',
size:10,
x:5,
y:5
@ -253,6 +255,12 @@ network.setOptions(options);
<td><code>Object</code></td>
<td>Exactly the same as the to object but with an arrowhead at the from node of the edge.</td>
</tr>
<tr>
<td class="indent">arrowStrikethrough</td>
<td>Boolean</td>
<td><code>true</code></td>
<td>When false, the edge stops at the arrow. This can be useful if you have thick lines and you want the arrow to end in a point. Middle arrows are not affected by this.</td>
</tr>
<tr class='toggle collapsible' onclick="toggleTable('optionTable','color', this);">
<td><span parent="color" class="right-caret"></span> color</td>
<td>Object or String</td>
@ -581,6 +589,12 @@ var options: {
in this object are defined.
</td>
</tr>
<tr parent="shadow" class="hidden">
<td class="indent">shadow.color</td>
<td>String</td>
<td><code>'rgba(0,0,0,0.5)'</code></td>
<td>The color size of the shadow as a string. Supported formats are 'rgb(255,255,255)', 'rgba(255,255,255,1)' and '#FFFFFF'.</td>
</tr>
<tr parent="shadow" class="hidden">
<td class="indent">shadow.size</td>
<td>Number</td>

+ 37
- 19
docs/network/index.html View File

@ -333,59 +333,55 @@ network.setOptions(options);
<tr><td id="event_clickToUse">clickToUse</td>
<td>Boolean</td>
<td>false</td>
<td>Locales object. By default only <code>'en'</code>, <code>'de'</code>, <code>'es'</code> and <code>'nl'</code> are supported. Take a look
at
the <a href="#locales" data-scroll=""
data-options="{ &quot;easing&quot;: &quot;easeInCubic&quot; }">locales
section below</a> for more explaination on how to customize this.
<td>When a Network is configured to be <code>clickToUse</code>, it will react to mouse and touch events only when active. When active, a blue shadow border is displayed around the Network. The network is set active by clicking on it, and is changed to inactive again by clicking outside the Network or by pressing the ESC key.
</td>
</tr>
<tr><td id="event_configure">configure</td>
<td>Object</td>
<td>Object</td>
<td href="./configure.html">All options in this object are explained in the configure module.</a>
<td>All options in this object are explained in the <a href="./configure.html">configure module</a>.
</td>
</tr>
<tr><td id="event_edges">edges</td>
<td>Object</td>
<td>Object</td>
<td href="./edges.html">All options in this object are explained in the edges module.</a>
<td>All options in this object are explained in the <a href="./edges.html">edges module</a>.
</td>
</tr>
<tr><td id="event_nodes">nodes</td>
<td>Object</td>
<td>Object</td>
<td href="./nodes.html">All options in this object are explained in the nodes module.</a>
<td>All options in this object are explained in the <a href="./nodes.html">nodes module</a>.
</td>
</tr>
<tr><td id="event_groups">groups</td>
<td>Object</td>
<td>Object</td>
<td href="./groups.html">All options in this object are explained in the groups module.</a>
<td>All options in this object are explained in the <a href="./groups.html">groups module</a>.
</td>
</tr>
<tr><td id="event_layout">layout</td>
<td>Object</td>
<td>Object</td>
<td href="./layout.html">All options in this object are explained in the layout module.</a>
<td>All options in this object are explained in the <a href="./layout.html">layout module</a>.
</td>
</tr>
<tr><td id="event_interaction">interaction</td>
<td>Object</td>
<td>Object</td>
<td href="./interaction.html">All options in this object are explained in the interaction module.</a>
<td>All options in this object are explained in the <a href="./interaction.html">interaction module</a>.
</td>
</tr>
<tr><td id="event_manipulation">manipulation</td>
<td>Object</td>
<td>Object</td>
<td href="./manipulation.html">All options in this object are explained in the manipulation module.</a>
<td>All options in this object are explained in the <a href="./manipulation.html">manipulation module</a>.
</td>
</tr>
<tr><td id="event_physics">physics</td>
<td>Object</td>
<td>Object</td>
<td href="./physics.html">All options in this object are explained in the physics module.</a>
<td>All options in this object are explained in the <a href="./physics.html">physics module</a>.
</td>
</tr>
</table>
@ -944,6 +940,28 @@ function releaseFunction (clusterPosition, containedNodesPositions) {
before selecting its own objects. <i>Does not fire events</i>.
</td>
</tr>
<tr class="collapsible toggle" onclick="toggleTable('methodTable','setSelection', this);">
<td colspan="2"><span parent="setSelection" class="right-caret" id="method_setSelection"></span> setSelection(
<code>Object selection</code>,
<code>[Object options]</code>)</td>
</tr>
<tr class="hidden" parent="setSelection">
<td class="midMethods">Returns: none</td>
<td>Sets the selection, wich must be an object like this:
<pre class="code">
{
nodes: [Array of nodeIds],
edges: [Array of edgeIds]
}</pre>
You can also pass only <code>nodes</code> or <code>edges</code> in <code>selection</code> object.
Available options are:
<pre class="code">
{
unselectAll: Boolean,
highlightEdges: Boolean
}</pre>
</td>
</tr>
<tr class="collapsible toggle" onclick="toggleTable('methodTable','unselectAll', this);">
<td colspan="2"><span parent="unselectAll" class="right-caret" id="method_unselectAll"></span> unselectAll()</td>
</tr>
@ -968,8 +986,8 @@ function releaseFunction (clusterPosition, containedNodesPositions) {
<td colspan="2"><span parent="clusterByHubsize" class="right-caret" id="method_getViewPosition"></span> getViewPosition()</td>
</tr>
<tr class="hidden" parent="clusterByHubsize">
<td class="midMethods">Returns: Number</td>
<td>Returns the current central focus point of the view.</td>
<td class="midMethods">Returns: Object</td>
<td>Returns the current central focus point of the view in the form: <code>{ x: {Number}, y: {Number} }</code></td>
</tr>
<tr class="collapsible toggle" onclick="toggleTable('methodTable','fit', this);">
@ -1301,19 +1319,19 @@ var options = {
</tr>
<tr><td id="event_hoverNode">hoverNode</td>
<td><code>{node: nodeId}</code></td>
<td>Fired interaction:{hover:true} and the mouse hovers over a node.</td>
<td>Fired if the option <code>interaction:{hover:true}</code> is enabled and the mouse hovers over a node.</td>
</tr>
<tr><td id="event_blurNode">blurNode</td>
<td><code>{node: nodeId}</code></td>
<td>Fired interaction:{hover:true} and the mouse moved away from a node it was hovering over before.</td>
<td>Fired if the option <code>interaction:{hover:true}</code> is enabled and the mouse moved away from a node it was hovering over before.</td>
</tr>
<tr><td id="event_hoverEdge">hoverEdge</td>
<td><code>{edge: edgeId}</code></td>
<td>Fired interaction:{hover:true} and the mouse hovers over an edge.</td>
<td>Fired if the option <code>interaction:{hover:true}</code> is enabled and the mouse hovers over an edge.</td>
</tr>
<tr><td id="event_blurEdge">blurEdge</td>
<td><code>{edge: edgeId}</code></td>
<td>Fired interaction:{hover:true} and the mouse moved away from an edge it was hovering over before.</td>
<td>Fired if the option <code>interaction:{hover:true}</code> is enabled and the mouse moved away from an edge it was hovering over before.</td>
</tr>
<tr><td id="event_zoom">zoom</td>
<td><code>{direction:'+'/'-', scale: Number}</code></td>

+ 14
- 2
docs/network/layout.html View File

@ -105,8 +105,13 @@ var options = {
hierarchical: {
enabled:false,
levelSeparation: 150,
direction: 'UD', // UD, DU, LR, RL
sortMethod: 'hubsize' // hubsize, directed
nodeSpacing: 100,
treeSpacing: 200,
blockShifting: true,
edgeMinimization: true,
parentCentralization: true,
direction: 'UD', // UD, DU, LR, RL
sortMethod: 'hubsize' // hubsize, directed
}
}
}
@ -132,6 +137,13 @@ network.setOptions(options);
<tr class='toggle collapsible' onclick="toggleTable('optionTable','hierarchical', this);"><td><span parent="repulsion" class="right-caret"></span> hierarchical</td><td>Object or Boolean</td><td><code>Object</code></td> <td>When true, the layout engine positions the nodes in a hierarchical fashion using default settings. For customization you can supply an object.</td></tr>
<tr parent="hierarchical" class="hidden"><td class="indent">hierarchical.enabled</td><td>Boolean</td><td><code>false</code></td> <td>Toggle the usage of the hierarchical layout system. If this option is not defined, it is set to true if any of the properties in this object are defined.</td></tr>
<tr parent="hierarchical" class="hidden"><td class="indent">hierarchical.levelSeparation</td><td>Number</td><td><code>150</code></td> <td>The distance between the different levels.</td></tr>
<tr parent="hierarchical" class="hidden"><td class="indent">hierarchical.nodeSpacing</td><td>Number</td><td><code>100</code></td> <td>Minimum distance between nodes on the free axis. This is only for the initial layout. If you enable physics, the node distance there will be the effective node distance.</td></tr>
<tr parent="hierarchical" class="hidden"><td class="indent">hierarchical.treeSpacing</td><td>Number</td><td><code>200</code></td> <td>Distance between different trees (independent networks). This is only for the initial layout. If you enable physics, the repulsion model will denote the distance between the trees.</td></tr>
<tr parent="hierarchical" class="hidden"><td class="indent">hierarchical.blockShifting</td><td>Boolean</td><td><code>true</code></td> <td>Method for reducing whitespace. Can be used alone or together with edge minimization. Each node will check for whitespace and will shift
it's branch along with it for as far as it can, respecting the nodeSpacing on any level. This is mainly for the initial layout. If you enable physics, they layout will be determined by the physics. This will greatly speed up the stabilization time though!</td></tr>
<tr parent="hierarchical" class="hidden"><td class="indent">hierarchical.edgeMinimization</td><td>Boolean</td><td><code>true</code></td> <td>Method for reducing whitespace. Can be used alone or together with block shifting. Enabling block shifting will usually speed up the layout process.
Each node will try to move along its free axis to reduce the total length of it's edges. This is mainly for the initial layout. If you enable physics, they layout will be determined by the physics. This will greatly speed up the stabilization time though!</td></tr>
<tr parent="hierarchical" class="hidden"><td class="indent">hierarchical.parentCentralization</td><td>Boolean</td><td><code>true</code></td> <td>When true, the parents nodes will be centered again after the the layout algorithm has been finished.</td></tr>
<tr parent="hierarchical" class="hidden"><td class="indent">hierarchical.direction</td><td>String</td><td><code>'UD'</code></td> <td>The direction of the hierarchical layout. The available options are: <code>UD, DU, LR, RL</code>. To simplify: up-down, down-up, left-right, right-left.</td></tr>
<tr parent="hierarchical" class="hidden"><td class="indent">hierarchical.sortMethod</td><td>String</td><td><code>'hubsize'</code></td> <td>The algorithm used to ascertain the levels of the nodes based on the data. The possible options are: <code>hubsize, directed</code>. <br><br>
Hubsize takes the nodes with the most edges and puts them at the top. From that the rest of the hierarchy is evaluated. <br><br>

+ 31
- 5
docs/network/nodes.html View File

@ -108,7 +108,7 @@
var options = {
nodes:{
borderWidth: 1,
borderWidthSelected: undefined,
borderWidthSelected: 2,
brokenImage:undefined,
color: {
border: '#2B7CE9',
@ -171,14 +171,18 @@ var options = {
},
shadow:{
enabled: false,
color: 'rgba(0,0,0,0.5)',
size:10,
x:5,
y:5
},
shape: 'ellipse',
shapeProperties: {
borderDashes: false, // only for shapes with a border
borderRadius: 6 // only for box shape
borderDashes: false, // only for borders
borderRadius: 6, // only for box shape
interpolation: false, // only for image and circularImage shapes
useImageSize: false, // only for image and circularImage shapes
useBorderWithImage: false // only for image shape
}
size: 25,
title: undefined,
@ -224,8 +228,8 @@ network.setOptions(options);
<tr>
<td>borderWidthSelected</td>
<td>Number</td>
<td><code>undefined</code></td>
<td>The width of the border of the node when it is selected. When undefined, the borderWidth is used</td>
<td><code>2</code></td>
<td>The width of the border of the node when it is selected. When undefined, the borderWidth * 2 is used.</td>
</tr>
<tr>
<td>brokenImage</td>
@ -590,6 +594,12 @@ mySize = minSize + diff * scale;
in this object are defined.
</td>
</tr>
<tr parent="shadow" class="hidden">
<td class="indent">shadow.color</td>
<td>String</td>
<td><code>'rgba(0,0,0,0.5)'</code></td>
<td>The color size of the shadow as a string. Supported formats are 'rgb(255,255,255)', 'rgba(255,255,255,1)' and '#FFFFFF'.</td>
</tr>
<tr parent="shadow" class="hidden">
<td class="indent">shadow.size</td>
<td>Number</td>
@ -643,6 +653,13 @@ mySize = minSize + diff * scale;
<td>This property is used only for the <code>box</code> shape. It allows you to determine the roundness of the corners of the shape.
</td>
</tr>
<tr parent="shapeProperties" class="hidden">
<td class="indent">shapeProperties.interpolation</td>
<td>Boolean</td>
<td><code>true</code></td>
<td>This property only applies to the <code>image</code> and <code>circularImage</code> shapes. When true, the image is resampled when scaled down, resulting in a nicer image at the cost of computional time.</i>
</td>
</tr>
<tr parent="shapeProperties" class="hidden">
<td class="indent">shapeProperties.useImageSize</td>
<td>Boolean</td>
@ -651,6 +668,15 @@ mySize = minSize + diff * scale;
if this is set to true, the image cannot be scaled with the value option!</i>
</td>
</tr>
<tr parent="shapeProperties" class="hidden">
<td class="indent">shapeProperties.useBorderWithImage</td>
<td>Boolean</td>
<td><code>false</code></td>
<td>This property only applies to the <code>image</code> shape.
When true, the color object is used. A rectangle with the background color is
drawn behind it and it has a border. This means all border options are taken into account.
</td>
</tr>
<tr>
<td>size</td>
<td>Number</td>

+ 59
- 3
docs/timeline/index.html View File

@ -115,7 +115,7 @@
<h2 id="Example">Example</h2>
<p>
The following code shows how to create a Timeline and provide it with data.
More examples can be found in the <a href="../examples">examples</a> directory.
More examples can be found in the <a href="../../timeline_examples.html">timeline examples</a> page.
</p>
<pre class="prettyprint lang-html">&lt;!DOCTYPE HTML&gt;
@ -666,6 +666,14 @@ function (option, path) {
and set repeat to weekly.
</td>
</tr>
<tr>
<td>itemsAlwaysDraggable</td>
<td>boolean</td>
<td><code>false</code></td>
<td>If true, all items in the Timeline are draggable without being selected. If false, only the selected item(s) are draggable.</td>
</tr>
<tr>
<td>locale</td>
<td>String</td>
@ -734,6 +742,16 @@ function (option, path) {
<td>Specifies the maximum height for the Timeline. Can be a number in pixels or a string like "300px".</td>
</tr>
<tr>
<td>maxMinorChars</td>
<td>number</td>
<td>7</td>
<td>
Specifies the maximum number of characters that should fit in minor grid labels.
If larger, less and wider grids will be drawn.
</td>
</tr>
<tr>
<td>min</td>
<td>Date or Number or String or Moment</td>
@ -769,6 +787,16 @@ function (option, path) {
Only applicable when option <code>selectable</code> is <code>true</code>.
</td>
</tr>
<tr>
<td style="font-size: 0.9em">multiselectPerGroup</td>
<td>boolean</td>
<td><code>false</code></td>
<td>
If true, selecting multiple items using shift+click will only select items residing in the same group as the <i>first</i> selected item.
Only applicable when option <code>selectable</code> and <code>multiselect</code> are <code>true</code>.
</td>
</tr>
<tr>
<td>onAdd</td>
@ -1122,6 +1150,12 @@ document.getElementById('myTimeline').onclick = function (event) {
</td>
</tr>
<tr>
<td>getItemRange()</td>
<td>Object</td>
<td>Get the range of all the items as an object containing <code>min: Date</code> and <code>max: Date</code>.</td>
</tr>
<tr>
<td>getSelection()</td>
<td>number[]</td>
@ -1196,6 +1230,15 @@ document.getElementById('myTimeline').onclick = function (event) {
</td>
</tr>
<tr>
<td>setCustomTimeTitle(title [, id])</td>
<td>none</td>
<td>Adjust the title attribute of a custom time bar.
Parameter <code>title</code> is the string to be set as title. Use empty string to hide the title completely.
Parameter <code>id</code> is the id of the custom time bar, and is <code>undefined</code> by default.
</td>
</tr>
<tr>
<td>setData({<br>&nbsp;&nbsp;groups: groups,<br>&nbsp;&nbsp;items: items<br>})</td>
<td>none</td>
@ -1305,6 +1348,11 @@ timeline.off('select', onSelect);
<th>Description</th>
</tr>
<tr>
<td>currentTimeTick</td>
<td>Fired when the current time bar redraws. The rate depends on the zoom level.</td>
</tr>
<tr>
<td>click</td>
<td>
@ -1344,8 +1392,16 @@ timeline.off('select', onSelect);
</td>
<td>Fired after the dragging of a group is finished.
</td>
</tr>
</tr>
<tr>
<td>changed</td>
<td>
Has no properties.
</td>
<td>Fired once after each graph redraw.
</td>
</tr>
<tr>
<td>rangechange</td>
<td>
@ -1492,7 +1548,7 @@ var items = new vis.DataSet([
};
</pre>
A full example is available here: <a href="../../examples/timeline/08_manipulation_callbacks.html">08_manipulation_callbacks.html</a>.
A full example is available here: <a href="../../examples/timeline/editing/editingItemsCallbacks.html">editingItemsCallbacks.html</a>.
<h2 id="Templates">Templates</h2>

+ 1
- 1
examples/graph2d/06_interpolation.html View File

@ -88,7 +88,7 @@
}
var options = {
dataPoints: false,
drawPoints: false,
dataAxis: {visible: false},
legend: true,
start: '2014-06-11',

+ 14
- 13
examples/graph2d/17_dynamicStyling.html View File

@ -79,7 +79,8 @@
<select id="fill" onchange="updateStyle()">
<option value="">none</option>
<option value="top">top</option>
<option value="bottom" selected="selected">bottom</option>
<option value="bottom">bottom</option>
<option value="zero" selected="selected">zero</option>
</select>
</td>
</tr>
@ -98,16 +99,16 @@
<td>Fill Opacity</td>
<td>
<select id="fillopacity" onchange="updateStyle()">
<option value="opacity:0.1;">0.1</option>
<option value="opacity:0.2;">0.2</option>
<option value="opacity:0.3;">0.3</option>
<option value="opacity:0.4;">0.4</option>
<option value="opacity:0.5;">0.5</option>
<option value="opacity:0.6;" selected="selected">0.6</option>
<option value="opacity:0.7;">0.7</option>
<option value="opacity:0.8;">0.8</option>
<option value="opacity:0.9;">0.9</option>
<option value="opacity:1;">1</option>
<option value="fill-opacity:0.1;">0.1</option>
<option value="fill-opacity:0.2;">0.2</option>
<option value="fill-opacity:0.3;">0.3</option>
<option value="fill-opacity:0.4;">0.4</option>
<option value="fill-opacity:0.5;">0.5</option>
<option value="fill-opacity:0.6;" selected="selected">0.6</option>
<option value="fill-opacity:0.7;">0.7</option>
<option value="fill-opacity:0.8;">0.8</option>
<option value="fill-opacity:0.9;">0.9</option>
<option value="fill-opacity:1;">1</option>
</select>
</td>
</tr>
@ -187,7 +188,7 @@
{x: '2014-06-11', y: 10, group: 0},
{x: '2014-06-12', y: 25, group: 0},
{x: '2014-06-13', y: 30, group: 0},
{x: '2014-06-14', y: 10, group: 0},
{x: '2014-06-14', y: -10, group: 0},
{x: '2014-06-15', y: 15, group: 0},
{x: '2014-06-16', y: 30, group: 0}
];
@ -210,7 +211,7 @@
style: 'square' // square, circle
},
shaded: {
orientation: 'bottom' // top, bottom
orientation: 'zero' // top, bottom
}
}
};

+ 117
- 0
examples/graph2d/20_shading.html View File

@ -0,0 +1,117 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Graph2d | Shading Example</title>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding">
<style type="text/css">
body, html {
font-family: sans-serif;
}
</style>
<script src="../../dist/vis.js"></script>
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" />
<script>(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');ga('create', 'UA-61231638-1', 'auto');ga('send', 'pageview');</script></head>
<body>
<h2>Graph2d | Shading Example</h2>
<div style="width:700px; font-size:14px; text-align: justify;">
This example shows the shading functionality within Graph2d.
</div>
<br />
<div id="visualization"></div>
<script type="text/javascript">
// create a dataSet with groups
var names = ['top', 'bottom', 'zero', 'none', 'group', 'none'];
var groups = new vis.DataSet();
groups.add({
id: 0,
content: names[0],
options: {
shaded: {
orientation: 'top'
}
}});
groups.add({
id: 1,
content: names[1],
options: {
shaded: {
orientation: 'bottom'
}
}});
groups.add({
id: 2,
content: names[2],
options: {
shaded: {
orientation: 'zero'
}
}});
groups.add({
id: 3,
options: {
excludeFromLegend: true
}
});
groups.add({
id: 4,
content: names[4],
options: {
shaded: {
orientation: 'group',
groupId: '3'
}
}
});
groups.add({
id: 5,
content: names[5]
});
var container = document.getElementById('visualization');
var items = [
{x: '2014-06-11', y: 0, group: 0},
{x: '2014-06-12', y: 15, group: 0},
{x: '2014-06-13', y: -15, group: 0},
{x: '2014-06-14', y: 0, group: 0},
{x: '2014-06-15', y: 0, group: 1},
{x: '2014-06-16', y: 15, group: 1},
{x: '2014-06-17', y: -15, group: 1},
{x: '2014-06-18', y: 0, group: 1},
{x: '2014-06-19', y: 0, group: 2},
{x: '2014-06-20', y: 15, group: 2},
{x: '2014-06-21', y: -15, group: 2},
{x: '2014-06-22', y: 0, group: 2},
{x: '2014-06-23', y: -2, group: 3},
{x: '2014-06-24', y: 13, group: 3},
{x: '2014-06-25', y: -17, group: 3},
{x: '2014-06-26', y: -2, group: 3},
{x: '2014-06-23', y: 2, group: 4},
{x: '2014-06-24', y: 17, group: 4},
{x: '2014-06-25', y: -13, group: 4},
{x: '2014-06-26', y: 2, group: 4},
{x: '2014-06-27', y: 0, group: 5},
{x: '2014-06-28', y: 15, group: 5},
{x: '2014-06-29', y: -15, group: 5},
{x: '2014-06-30', y: 0, group: 5}
];
var dataset = new vis.DataSet(items);
var options = {
legend: true,
start: '2014-06-07',
end: '2014-07-03'
};
var graph2d = new vis.Graph2d(container, dataset, groups, options);
</script>
</body>
</html>

+ 2
- 0
examples/network/datasources/largeHierarchicalDataset.js
File diff suppressed because it is too large
View File


+ 63
- 0
examples/network/exampleUtil.js View File

@ -66,5 +66,68 @@ function getScaleFreeNetwork(nodeCount) {
}
}
return {nodes:nodes, edges:edges};
}
var randomSeed = 764; // Math.round(Math.random()*1000);
function seededRandom() {
var x = Math.sin(randomSeed++) * 10000;
return x - Math.floor(x);
}
function getScaleFreeNetworkSeeded(nodeCount, seed) {
if (seed) {
randomSeed = Number(seed);
}
var nodes = [];
var edges = [];
var connectionCount = [];
var edgesId = 0;
// randomly create some nodes and edges
for (var i = 0; i < nodeCount; i++) {
nodes.push({
id: i,
label: String(i)
});
connectionCount[i] = 0;
// create edges in a scale-free-network way
if (i == 1) {
var from = i;
var to = 0;
edges.push({
id: edgesId++,
from: from,
to: to
});
connectionCount[from]++;
connectionCount[to]++;
}
else if (i > 1) {
var conn = edges.length * 2;
var rand = Math.floor(seededRandom() * conn);
var cum = 0;
var j = 0;
while (j < connectionCount.length && cum < rand) {
cum += connectionCount[j];
j++;
}
var from = i;
var to = j;
edges.push({
id: edgesId++,
from: from,
to: to
});
connectionCount[from]++;
connectionCount[to]++;
}
}
return {nodes:nodes, edges:edges};
}

+ 1
- 1
examples/network/layout/hierarchicalLayout.html View File

@ -57,7 +57,7 @@
}
</script>
<script src="../googleAnalytics.js"></script>
<script src="../../googleAnalytics.js"></script>
</head>
<body onload="draw();">

+ 1
- 1
examples/network/layout/hierarchicalLayoutMethods.html View File

@ -19,7 +19,7 @@
<script type="text/javascript">
var network = null;
var layoutMethod = "hubsize";
var layoutMethod = "directed";
function destroy() {
if (network !== null) {

+ 107
- 102
examples/network/layout/hierarchicalLayoutUserdefined.html View File

@ -1,120 +1,125 @@
<!doctype html>
<html>
<head>
<title>Network | Hierarchical Layout, userDefined</title>
<style type="text/css">
body {
font: 10pt sans;
}
#mynetwork {
width: 600px;
height: 600px;
border: 1px solid lightgray;
}
</style>
<title>Network | Hierarchical Layout, userDefined</title>
<style type="text/css">
body {
font: 10pt sans;
}
#mynetwork {
width: 600px;
height: 600px;
border: 1px solid lightgray;
}
</style>
<script type="text/javascript" src="../../../dist/vis.js"></script>
<link href="../../../dist/vis.css" rel="stylesheet" type="text/css" />
<link href="../../../dist/vis.css" rel="stylesheet" type="text/css"/>
<script type="text/javascript">
var nodes = null;
var edges = null;
var network = null;
var directionInput = document.getElementById("direction");
<script type="text/javascript">
var nodes = null;
var edges = null;
var network = null;
var directionInput = document.getElementById("direction");
function destroy() {
if (network !== null) {
network.destroy();
network = null;
}
}
function draw() {
destroy();
nodes = [];
edges = [];
var connectionCount = [];
// randomly create some nodes and edges
for (var i = 0; i < 15; i++) {
nodes.push({id: i,label: String(i)});
}
edges.push({from: 0, to: 1 });
edges.push({from: 0, to: 6 });
edges.push({from: 0, to: 13});
edges.push({from: 0, to: 11});
edges.push({from: 1, to: 2 });
edges.push({from: 2, to: 3 });
edges.push({from: 2, to: 4 });
edges.push({from: 3, to: 5 });
edges.push({from: 1, to: 10});
edges.push({from: 1, to: 7 });
edges.push({from: 2, to: 8 });
edges.push({from: 2, to: 9 });
edges.push({from: 3, to: 14});
edges.push({from: 1, to: 12});
nodes[0]["level"] = 0;
nodes[1]["level"] = 1;
nodes[2]["level"] = 3;
nodes[3]["level"] = 4;
nodes[4]["level"] = 4;
nodes[5]["level"] = 5;
nodes[6]["level"] = 1;
nodes[7]["level"] = 2;
nodes[8]["level"] = 4;
nodes[9]["level"] = 4;
nodes[10]["level"] = 2;
nodes[11]["level"] = 1;
nodes[12]["level"] = 2;
nodes[13]["level"] = 1;
nodes[14]["level"] = 5;
// create a network
var container = document.getElementById('mynetwork');
var data = {
nodes: nodes,
edges: edges
};
var options = {
edges: {
smooth: {
type:'cubicBezier',
forceDirection: (directionInput.value == "UD" || directionInput.value == "DU") ? 'vertical' : 'horizontal',
roundness: 0.4
}
},
layout: {
hierarchical:{
direction: directionInput.value
function destroy() {
if (network !== null) {
network.destroy();
network = null;
}
}
};
network = new vis.Network(container, data, options);
// add event listeners
network.on('select', function(params) {
document.getElementById('selection').innerHTML = 'Selection: ' + params.nodes;
});
}
function draw() {
destroy();
nodes = [];
edges = [];
var connectionCount = [];
</script>
<script src="../../googleAnalytics.js"></script>
// randomly create some nodes and edges
for (var i = 0; i < 15; i++) {
nodes.push({id: i, label: String(i)});
}
edges.push({from: 0, to: 1});
edges.push({from: 0, to: 6});
edges.push({from: 0, to: 13});
edges.push({from: 0, to: 11});
edges.push({from: 1, to: 2});
edges.push({from: 2, to: 3});
edges.push({from: 2, to: 4});
edges.push({from: 3, to: 5});
edges.push({from: 1, to: 10});
edges.push({from: 1, to: 7});
edges.push({from: 2, to: 8});
edges.push({from: 2, to: 9});
edges.push({from: 3, to: 14});
edges.push({from: 1, to: 12});
nodes[0]["level"] = 0;
nodes[1]["level"] = 1;
nodes[2]["level"] = 3;
nodes[3]["level"] = 4;
nodes[4]["level"] = 4;
nodes[5]["level"] = 5;
nodes[6]["level"] = 1;
nodes[7]["level"] = 2;
nodes[8]["level"] = 4;
nodes[9]["level"] = 4;
nodes[10]["level"] = 2;
nodes[11]["level"] = 1;
nodes[12]["level"] = 2;
nodes[13]["level"] = 1;
nodes[14]["level"] = 5;
// create a network
var container = document.getElementById('mynetwork');
var data = {
nodes: nodes,
edges: edges
};
var options = {
edges: {
smooth: {
type: 'cubicBezier',
forceDirection: (directionInput.value == "UD" || directionInput.value == "DU") ? 'vertical' : 'horizontal',
roundness: 0.4
}
},
layout: {
hierarchical: {
direction: directionInput.value
}
},
physics:false
};
network = new vis.Network(container, data, options);
// add event listeners
network.on('select', function (params) {
document.getElementById('selection').innerHTML = 'Selection: ' + params.nodes;
});
}
</script>
<script src="../../googleAnalytics.js"></script>
</head>
<body onload="draw();">
<h2>Hierarchical Layout - User-defined</h2>
<div style="width:700px; font-size:14px; text-align: justify;">
This example shows a user-defined hierarchical layout. If the user defines levels for nodes but does not do so for all nodes, an alert will show up and hierarchical layout will be disabled. Either all or none can be defined.
This example shows a user-defined hierarchical layout. If the user defines levels for nodes but does not do so for
all nodes, an alert will show up and hierarchical layout will be disabled. Either all or none can be defined.
If the smooth curves appear to be inverted, the direction of the edge is not in the same direction as the network.
</div>
<p>
<input type="button" id="btn-UD" value="Up-Down">
<input type="button" id="btn-DU" value="Down-Up">
<input type="button" id="btn-LR" value="Left-Right">
<input type="button" id="btn-RL" value="Right-Left">
<input type="hidden" id='direction' value="UD">
<input type="button" id="btn-UD" value="Up-Down">
<input type="button" id="btn-DU" value="Down-Up">
<input type="button" id="btn-LR" value="Left-Right">
<input type="button" id="btn-RL" value="Right-Left">
<input type="hidden" id='direction' value="UD">
</p>
<div id="mynetwork"></div>
@ -123,22 +128,22 @@
<script language="JavaScript">
var directionInput = document.getElementById("direction");
var btnUD = document.getElementById("btn-UD");
btnUD.onclick = function() {
btnUD.onclick = function () {
directionInput.value = "UD";
draw();
};
var btnDU = document.getElementById("btn-DU");
btnDU.onclick = function() {
btnDU.onclick = function () {
directionInput.value = "DU";
draw();
};
var btnLR = document.getElementById("btn-LR");
btnLR.onclick = function() {
btnLR.onclick = function () {
directionInput.value = "LR";
draw();
};
var btnRL = document.getElementById("btn-RL");
btnRL.onclick = function() {
btnRL.onclick = function () {
directionInput.value = "RL";
draw();
};

+ 89
- 0
examples/network/layout/hierarchicalLayoutWithoutPhysics.html View File

@ -0,0 +1,89 @@
<html>
<head>
<meta charset="utf-8">
<title>Hierarchical Layout without Physics</title>
<script type="text/javascript" src="../../../dist/vis.js"></script>
<script type="text/javascript" src="../datasources/largeHierarchicalDataset.js"></script>
<link href="../../../dist/vis.css" rel="stylesheet" type="text/css" />
<style type="text/css">
#network{
width: 1000px;
height: 400px;
border: 1px solid lightgray;
}
td {
vertical-align:top;
}
table {
width:800px;
}
</style>
</head>
<body>
<h1>Hierarchical Layout without Physics</h1>
The hierarchical layout can now be controlled without the use of physics. This is much quicker. The options for this are: <br /><br />
<table>
<tr>
<td width="150px"><code>levelSeparation</code></td>
<td width="400px">Distance between levels.</td>
</tr>
<tr>
<td><code>nodeSpacing</code></td>
<td>Minimum distance between nodes on the free axis.</td>
</tr>
<tr>
<td><code>treeSpacing</code></td>
<td>Distance between different trees (independent networks).</td>
</tr>
<tr>
<td><code>blockShifting</code></td>
<td>Method for reducing whitespace. Can be used alone or together with edge minimization. Each node will check for whitespace and will shift
it's branch along with it for as far as it can, respecting the nodeSpacing on any level.</td>
</tr>
<tr>
<td><code>edgeMinimization</code></td>
<td>Method for reducing whitespace. Can be used alone or together with block shifting. Enabling block shifting will usually speed up the layout process.
Each node will try to move along its free axis to reduce the total length of it's edges.</td>
</tr>
<tr>
<td><code>parentCentralization</code></td>
<td>When true, the parents nodes will be centered again after the the layout algorithm has been finished.</td>
</tr>
</table>
<br /><br />
Play with the settings below the network and see how the layout changes!
<div id="network"></div>
<script>
var data = {
nodes: nodes,
edges: edges
};
// create a network
var container = document.getElementById('network');
var options = {
layout: {
hierarchical: {
direction: "UD",
sortMethod: "directed"
}
},
interaction: {dragNodes :false},
physics: {
enabled: false
},
configure: {
filter: function (option, path) {
if (path.indexOf('hierarchical') !== -1) {
return true;
}
return false;
},
showButton:false
}
};
var network = new vis.Network(container, data, options);
</script>
</body>
</html>

+ 3
- 5
examples/network/nodeStyles/customGroups.html View File

@ -26,12 +26,9 @@
</head>
<body>
<i class="fa fa-flag"></i> We use an icon once in the DOM so the CSS for fontAwesome is loaded.</h2>
<div id="mynetwork"></div>
<script type="text/javascript">
var color = 'gray';
var len = undefined;
var nodes = [
{id: 0, label: "0", group: 'source'},
{id: 1, label: "1", group: 'icons'},
@ -128,7 +125,8 @@
}
}
};
network = new vis.Network(container, data, options);
var network = new vis.Network(container, data, options);
</script>
</body>
</html>

+ 105
- 0
examples/network/nodeStyles/imagesWithBorders.html View File

@ -0,0 +1,105 @@
<!doctype html>
<html>
<head>
<title>Network | Images With Borders</title>
<style type="text/css">
body {
font: 10pt arial;
}
#mynetwork {
width: 800px;
height: 800px;
border: 1px solid lightgray;
background-color:#333333;
}
</style>
<script type="text/javascript" src="../../../dist/vis.js"></script>
<link href="../../../dist/vis.css" rel="stylesheet" type="text/css" />
<script type="text/javascript">
var DIR = 'img/soft-scraps-icons/';
var nodes = null;
var edges = null;
var network = null;
// Called when the Visualization API is loaded.
function draw() {
// create people.
// value corresponds with the age of the person
var DIR = '../img/indonesia/';
nodes = [
{id: 1, shape: 'image', image: DIR + '1.png'},
{id: 2, shape: 'image', image: DIR + '2.png'},
{id: 3, shape: 'image', image: DIR + '3.png'},
{id: 4, shape: 'image', image: DIR + '4.png', label:"pictures by this guy!"},
{id: 5, shape: 'image', image: DIR + '5.png'},
{id: 6, shape: 'image', image: DIR + '6.png'},
{id: 7, shape: 'image', image: DIR + '7.png'},
{id: 8, shape: 'image', image: DIR + '8.png'},
{id: 9, shape: 'image', image: DIR + '9.png'},
{id: 10, shape: 'image', image: DIR + '10.png'},
{id: 11, shape: 'image', image: DIR + '11.png'},
{id: 12, shape: 'image', image: DIR + '12.png'},
{id: 13, shape: 'image', image: DIR + '13.png'},
{id: 14, shape: 'image', image: DIR + '14.png'},
{id: 15, shape: 'image', image: DIR + 'missing.png', brokenImage: DIR + 'missingBrokenImage.png', label:"when images\nfail\nto load"},
{id: 16, shape: 'image', image: DIR + 'anotherMissing.png', brokenImage: DIR + '9.png', label:"fallback image in action"}
];
// create connections between people
// value corresponds with the amount of contact between two people
edges = [
{from: 1, to: 2},
{from: 2, to: 3},
{from: 2, to: 4},
{from: 4, to: 5},
{from: 4, to: 10},
{from: 4, to: 6},
{from: 6, to: 7},
{from: 7, to: 8},
{from: 8, to: 9},
{from: 8, to: 10},
{from: 10, to: 11},
{from: 11, to: 12},
{from: 12, to: 13},
{from: 13, to: 14},
{from: 9, to: 16}
];
// create a network
var container = document.getElementById('mynetwork');
var data = {
nodes: nodes,
edges: edges
};
var options = {
nodes: {
borderWidth:4,
size:30,
color: {
border: '#406897',
background: '#6AAFFF'
},
font:{color:'#eeeeee'},
shapeProperties: {
useBorderWithImage:true
}
},
edges: {
color: 'lightgray'
}
};
network = new vis.Network(container, data, options);
}
</script>
<script src="../../googleAnalytics.js"></script>
</head>
<body onload="draw()">
<div id="mynetwork"></div>
</body>
</html>

+ 1
- 1
examples/network/nodeStyles/shadows.html View File

@ -87,7 +87,7 @@
{from: 25, to: 24},
{from: 26, to: 25},
{from: 25, to: 7},
{from: 28, to: 27},
{from: 28, to: 27, shadow:{color:'rgb(0,255,0)'}},
{from: 29, to: 28},
{from: 28, to: 0}
]

+ 4
- 4
examples/network/other/clustering.html View File

@ -80,7 +80,7 @@ Click any of the buttons below to cluster the network. On every push the network
network.openCluster(params.nodes[0]);
}
}
})
});
function clusterByCid() {
network.setData(data);
@ -89,7 +89,7 @@ Click any of the buttons below to cluster the network. On every push the network
return childOptions.cid == 1;
},
clusterNodeProperties: {id:'cidCluster', borderWidth:3, shape:'database'}
}
};
network.cluster(clusterOptionsByData);
}
function clusterByColor() {
@ -111,7 +111,7 @@ Click any of the buttons below to cluster the network. On every push the network
return clusterOptions;
},
clusterNodeProperties: {id: 'cluster:' + color, borderWidth: 3, shape: 'database', color:color, label:'color:' + color}
}
};
network.cluster(clusterOptionsByData);
}
}
@ -131,7 +131,7 @@ Click any of the buttons below to cluster the network. On every push the network
return clusterOptions;
},
clusterNodeProperties: {borderWidth:3, shape:'box', font:{size:30}}
}
};
network.clusterByHubsize(undefined, clusterOptionsByData);
}

+ 5
- 1
examples/network/other/clusteringByZoom.html View File

@ -72,7 +72,7 @@ Stabilize when clustering:
nodes: nodes,
edges: edges
};
var options = {layout: {randomSeed: 8}};
var options = {layout: {randomSeed: 8}, physics:{adaptiveTimestep:false}};
var network = new vis.Network(container, data, options);
// set the first initial zoom level
@ -125,6 +125,8 @@ Stabilize when clustering:
}
network.clusterOutliers(clusterOptionsByData);
if (document.getElementById('stabilizeCheckbox').checked === true) {
// since we use the scale as a unique identifier, we do NOT want to fit after the stabilization
network.setOptions({physics:{stabilization:{fit: false}}});
network.stabilize();
}
}
@ -145,6 +147,8 @@ Stabilize when clustering:
}
clusters = newClusters;
if (declustered === true && document.getElementById('stabilizeCheckbox').checked === true) {
// since we use the scale as a unique identifier, we do NOT want to fit after the stabilization
network.setOptions({physics:{stabilization:{fit: false}}});
network.stabilize();
}
}

+ 11
- 0
examples/network/other/configuration.html View File

@ -52,6 +52,15 @@
configure: true
};
network = new vis.Network(container, data, options);
network.on("configChange", function() {
// this will immediately fix the height of the configuration
// wrapper to prevent unecessary scrolls in chrome.
// see https://github.com/almende/vis/issues/1568
var div = container.getElementsByClassName('vis-configuration-wrapper')[0];
div.style["height"] = div.getBoundingClientRect().height + "px";
});
}
</script>
<script src="../../googleAnalytics.js"></script>
@ -64,6 +73,8 @@
You can also supply a custom filter function or filter string. You can press the generate options button below to have an options object printed. You can then use
this in the network.
</p>
<p><b>Note:</b> The configurator is recreated in the dom tree on input change. This may cause undesired scrolls in your application. In order to avoid this, explicitly set the height of the configurator (see this example's source code).
</p>
<br />
<div id="mynetwork"></div>

+ 4
- 4
gulpfile.js View File

@ -132,13 +132,13 @@ gulp.task('bundle-css', ['clean'], function () {
});
gulp.task('copy', ['clean'], function () {
var network = gulp.src('./lib/network/img/**/*')
var network = gulp.src('./lib/network/img/**/*')
.pipe(gulp.dest(DIST + '/img/network'));
var timeline = gulp.src('./lib/timeline/img/**/*')
var timeline = gulp.src('./lib/timeline/img/**/*')
.pipe(gulp.dest(DIST + '/img/timeline'));
return merge(network, timeline);
return merge(network, timeline);
});
gulp.task('minify', ['bundle-js'], function (cb) {
@ -148,7 +148,7 @@ gulp.task('minify', ['bundle-js'], function (cb) {
// any issues when concatenating the file downstream (the file ends
// with a comment).
fs.writeFileSync(DIST + '/' + VIS_MIN_JS, result.code + '\n');
fs.writeFileSync(DIST + '/' + VIS_MAP, result.map);
fs.writeFileSync(DIST + '/' + VIS_MAP, result.map.replace(/"\.\/dist\//g, '"'));
cb();
});

+ 3
- 2
index.js View File

@ -22,8 +22,8 @@ exports.graph3d = {
exports.Timeline = require('./lib/timeline/Timeline');
exports.Graph2d = require('./lib/timeline/Graph2d');
exports.timeline = {
Core: require('./lib/timeline/Core'),
DateUtil: require('./lib/timeline/DateUtil'),
DataStep: require('./lib/timeline/DataStep'),
Range: require('./lib/timeline/Range'),
stack: require('./lib/timeline/Stack'),
TimeStep: require('./lib/timeline/TimeStep'),
@ -37,13 +37,14 @@ exports.timeline = {
RangeItem: require('./lib/timeline/component/item/RangeItem')
},
BackgroundGroup: require('./lib/timeline/component/BackgroundGroup'),
Component: require('./lib/timeline/component/Component'),
CurrentTime: require('./lib/timeline/component/CurrentTime'),
CustomTime: require('./lib/timeline/component/CustomTime'),
DataAxis: require('./lib/timeline/component/DataAxis'),
DataScale: require('./lib/timeline/component/DataScale'),
GraphGroup: require('./lib/timeline/component/GraphGroup'),
Group: require('./lib/timeline/component/Group'),
BackgroundGroup: require('./lib/timeline/component/BackgroundGroup'),
ItemSet: require('./lib/timeline/component/ItemSet'),
Legend: require('./lib/timeline/component/Legend'),
LineGraph: require('./lib/timeline/component/LineGraph'),

+ 12
- 2
lib/DOMutil.js View File

@ -36,6 +36,16 @@ exports.cleanupElements = function(JSONcontainer) {
}
};
/**
* Ensures that all elements are removed first up so they can be recreated cleanly
* @param JSONcontainer
*/
exports.resetElements = function(JSONcontainer) {
exports.prepareElements(JSONcontainer);
exports.cleanupElements(JSONcontainer);
exports.prepareElements(JSONcontainer);
}
/**
* Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer
* the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this.
@ -149,8 +159,8 @@ exports.drawPoint = function(x, y, groupTemplate, JSONcontainer, svgContainer, l
point.setAttributeNS(null, "height", groupTemplate.size);
}
if (groupTemplate.style !== undefined) {
point.setAttributeNS(null, "style", groupTemplate.style);
if (groupTemplate.styles !== undefined) {
point.setAttributeNS(null, "style", groupTemplate.styles);
}
point.setAttributeNS(null, "class", groupTemplate.className + " vis-point");
//handle label

+ 158
- 125
lib/DataSet.js View File

@ -60,15 +60,15 @@ function DataSet (data, options) {
// all variants of a Date are internally stored as Date, so we can convert
// from everything to everything (also from ISODate to Number for example)
if (this._options.type) {
for (var field in this._options.type) {
if (this._options.type.hasOwnProperty(field)) {
var value = this._options.type[field];
if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') {
this._type[field] = 'Date';
}
else {
this._type[field] = value;
}
var fields = Object.keys(this._options.type);
for (var i = 0, len = fields.length; i < len; i++) {
var field = fields[i];
var value = this._options.type[field];
if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') {
this._type[field] = 'Date';
}
else {
this._type[field] = value;
}
}
}
@ -184,7 +184,7 @@ DataSet.prototype._trigger = function (event, params, senderId) {
subscribers = subscribers.concat(this._subscribers['*']);
}
for (var i = 0; i < subscribers.length; i++) {
for (var i = 0, len = subscribers.length; i < len; i++) {
var subscriber = subscribers[i];
if (subscriber.callback) {
subscriber.callback(event, params, senderId || null);
@ -236,6 +236,7 @@ DataSet.prototype.add = function (data, senderId) {
DataSet.prototype.update = function (data, senderId) {
var addedIds = [];
var updatedIds = [];
var oldData = [];
var updatedData = [];
var me = this;
var fieldId = me._fieldId;
@ -243,10 +244,12 @@ DataSet.prototype.update = function (data, senderId) {
var addOrUpdate = function (item) {
var id = item[fieldId];
if (me._data[id]) {
var oldItem = util.extend({}, me._data[id]);
// update item
id = me._updateItem(item);
updatedIds.push(id);
updatedData.push(item);
oldData.push(oldItem);
}
else {
// add new item
@ -258,7 +261,11 @@ DataSet.prototype.update = function (data, senderId) {
if (Array.isArray(data)) {
// Array
for (var i = 0, len = data.length; i < len; i++) {
addOrUpdate(data[i]);
if (data[i] instanceof Object){
addOrUpdate(data[i]);
} else {
console.warn('Ignoring input item, which is not an object at index ' + i);
}
}
}
else if (data instanceof Object) {
@ -273,7 +280,15 @@ DataSet.prototype.update = function (data, senderId) {
this._trigger('add', {items: addedIds}, senderId);
}
if (updatedIds.length) {
this._trigger('update', {items: updatedIds, data: updatedData}, senderId);
var props = { items: updatedIds, oldData: oldData, data: updatedData };
// TODO: remove deprecated property 'data' some day
//Object.defineProperty(props, 'data', {
// 'get': (function() {
// console.warn('Property data is deprecated. Use DataSet.get(ids) to retrieve the new data, use the oldData property on this object to get the old data');
// return updatedData;
// }).bind(this)
//});
this._trigger('update', props, senderId);
}
return addedIds.concat(updatedIds);
@ -340,13 +355,13 @@ DataSet.prototype.get = function (args) {
// build options
var type = options && options.type || this._options.type;
var filter = options && options.filter;
var items = [], item, itemId, i, len;
var items = [], item, itemIds, itemId, i, len;
// convert items
if (id != undefined) {
// return a single item
item = me._getItem(id, type);
if (filter && !filter(item)) {
if (item && filter && !filter(item)) {
item = null;
}
}
@ -361,12 +376,12 @@ DataSet.prototype.get = function (args) {
}
else {
// return all items
for (itemId in this._data) {
if (this._data.hasOwnProperty(itemId)) {
item = me._getItem(itemId, type);
if (!filter || filter(item)) {
items.push(item);
}
itemIds = Object.keys(this._data);
for (i = 0, len = itemIds.length; i < len; i++) {
itemId = itemIds[i];
item = me._getItem(itemId, type);
if (!filter || filter(item)) {
items.push(item);
}
}
}
@ -391,9 +406,11 @@ DataSet.prototype.get = function (args) {
// return the results
if (returnType == 'Object') {
var result = {};
for (i = 0; i < items.length; i++) {
result[items[i].id] = items[i];
var result = {},
resultant;
for (i = 0, len = items.length; i < len; i++) {
resultant = items[i];
result[resultant.id] = resultant;
}
return result;
}
@ -422,6 +439,7 @@ DataSet.prototype.getIds = function (options) {
filter = options && options.filter,
order = options && options.order,
type = options && options.type || this._options.type,
itemIds = Object.keys(data),
i,
len,
id,
@ -434,29 +452,27 @@ DataSet.prototype.getIds = function (options) {
if (order) {
// create ordered list
items = [];
for (id in data) {
if (data.hasOwnProperty(id)) {
item = this._getItem(id, type);
if (filter(item)) {
items.push(item);
}
for (i = 0, len = itemIds.length; i < len; i++) {
id = itemIds[i];
item = this._getItem(id, type);
if (filter(item)) {
items.push(item);
}
}
this._sort(items, order);
for (i = 0, len = items.length; i < len; i++) {
ids[i] = items[i][this._fieldId];
ids.push(items[i][this._fieldId]);
}
}
else {
// create unordered list
for (id in data) {
if (data.hasOwnProperty(id)) {
item = this._getItem(id, type);
if (filter(item)) {
ids.push(item[this._fieldId]);
}
for (i = 0, len = itemIds.length; i < len; i++) {
id = itemIds[i];
item = this._getItem(id, type);
if (filter(item)) {
ids.push(item[this._fieldId]);
}
}
}
@ -466,25 +482,23 @@ DataSet.prototype.getIds = function (options) {
if (order) {
// create an ordered list
items = [];
for (id in data) {
if (data.hasOwnProperty(id)) {
items.push(data[id]);
}
for (i = 0, len = itemIds.length; i < len; i++) {
id = itemIds[i];
items.push(data[id]);
}
this._sort(items, order);
for (i = 0, len = items.length; i < len; i++) {
ids[i] = items[i][this._fieldId];
ids.push(items[i][this._fieldId]);
}
}
else {
// create unordered list
for (id in data) {
if (data.hasOwnProperty(id)) {
item = data[id];
ids.push(item[this._fieldId]);
}
for (i = 0, len = itemIds.length; i < len; i++) {
id = itemIds[i];
item = data[id];
ids.push(item[this._fieldId]);
}
}
}
@ -514,6 +528,9 @@ DataSet.prototype.forEach = function (callback, options) {
var filter = options && options.filter,
type = options && options.type || this._options.type,
data = this._data,
itemIds = Object.keys(data),
i,
len,
item,
id;
@ -521,7 +538,7 @@ DataSet.prototype.forEach = function (callback, options) {
// execute forEach on ordered list
var items = this.get(options);
for (var i = 0, len = items.length; i < len; i++) {
for (i = 0, len = items.length; i < len; i++) {
item = items[i];
id = item[this._fieldId];
callback(item, id);
@ -529,12 +546,11 @@ DataSet.prototype.forEach = function (callback, options) {
}
else {
// unordered
for (id in data) {
if (data.hasOwnProperty(id)) {
item = this._getItem(id, type);
if (!filter || filter(item)) {
callback(item, id);
}
for (i = 0, len = itemIds.length; i < len; i++) {
id = itemIds[i];
item = this._getItem(id, type);
if (!filter || filter(item)) {
callback(item, id);
}
}
}
@ -556,15 +572,18 @@ DataSet.prototype.map = function (callback, options) {
type = options && options.type || this._options.type,
mappedItems = [],
data = this._data,
itemIds = Object.keys(data),
i,
len,
id,
item;
// convert and filter items
for (var id in data) {
if (data.hasOwnProperty(id)) {
item = this._getItem(id, type);
if (!filter || filter(item)) {
mappedItems.push(callback(item, id));
}
for (i = 0, len = itemIds.length; i < len; i++) {
id = itemIds[i];
item = this._getItem(id, type);
if (!filter || filter(item)) {
mappedItems.push(callback(item, id));
}
}
@ -588,17 +607,23 @@ DataSet.prototype._filterFields = function (item, fields) {
return item;
}
var filteredItem = {};
var filteredItem = {},
itemFields = Object.keys(item),
len = itemFields.length,
i,
field;
if(Array.isArray(fields)){
for (var field in item) {
if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
for (i = 0; i < len; i++) {
field = itemFields[i];
if (fields.indexOf(field) != -1) {
filteredItem[field] = item[field];
}
}
}else{
for (var field in item) {
if (item.hasOwnProperty(field) && fields.hasOwnProperty(field)) {
for (i = 0; i < len; i++) {
field = itemFields[i];
if (fields.hasOwnProperty(field)) {
filteredItem[fields[field]] = item[field];
}
}
@ -683,7 +708,7 @@ DataSet.prototype._remove = function (id) {
}
else if (id instanceof Object) {
var itemId = id[this._fieldId];
if (itemId && this._data[itemId]) {
if (itemId !== undefined && this._data[itemId]) {
delete this._data[itemId];
this.length--;
return itemId;
@ -715,17 +740,19 @@ DataSet.prototype.clear = function (senderId) {
*/
DataSet.prototype.max = function (field) {
var data = this._data,
itemIds = Object.keys(data),
max = null,
maxField = null;
for (var id in data) {
if (data.hasOwnProperty(id)) {
var item = data[id];
var itemField = item[field];
if (itemField != null && (!max || itemField > maxField)) {
max = item;
maxField = itemField;
}
maxField = null,
i,
len;
for (i = 0, len = itemIds.length; i < len; i++) {
var id = itemIds[i];
var item = data[id];
var itemField = item[field];
if (itemField != null && (!max || itemField > maxField)) {
max = item;
maxField = itemField;
}
}
@ -739,17 +766,19 @@ DataSet.prototype.max = function (field) {
*/
DataSet.prototype.min = function (field) {
var data = this._data,
itemIds = Object.keys(data),
min = null,
minField = null;
for (var id in data) {
if (data.hasOwnProperty(id)) {
var item = data[id];
var itemField = item[field];
if (itemField != null && (!min || itemField < minField)) {
min = item;
minField = itemField;
}
minField = null,
i,
len;
for (i = 0, len = itemIds.length; i < len; i++) {
var id = itemIds[i];
var item = data[id];
var itemField = item[field];
if (itemField != null && (!min || itemField < minField)) {
min = item;
minField = itemField;
}
}
@ -765,31 +794,33 @@ DataSet.prototype.min = function (field) {
*/
DataSet.prototype.distinct = function (field) {
var data = this._data;
var itemIds = Object.keys(data);
var values = [];
var fieldType = this._options.type && this._options.type[field] || null;
var count = 0;
var i;
for (var prop in data) {
if (data.hasOwnProperty(prop)) {
var item = data[prop];
var value = item[field];
var exists = false;
for (i = 0; i < count; i++) {
if (values[i] == value) {
exists = true;
break;
}
}
if (!exists && (value !== undefined)) {
values[count] = value;
count++;
var i,
j,
len;
for (i = 0, len = itemIds.length; i < len; i++) {
var id = itemIds[i];
var item = data[id];
var value = item[field];
var exists = false;
for (j = 0; j < count; j++) {
if (values[j] == value) {
exists = true;
break;
}
}
if (!exists && (value !== undefined)) {
values[count] = value;
count++;
}
}
if (fieldType) {
for (i = 0; i < values.length; i++) {
for (i = 0, len = values.length; i < len; i++) {
values[i] = util.convert(values[i], fieldType);
}
}
@ -819,12 +850,14 @@ DataSet.prototype._addItem = function (item) {
item[this._fieldId] = id;
}
var d = {};
for (var field in item) {
if (item.hasOwnProperty(field)) {
var fieldType = this._type[field]; // type may be undefined
d[field] = util.convert(item[field], fieldType);
}
var d = {},
fields = Object.keys(item),
i,
len;
for (i = 0, len = fields.length; i < len; i++) {
var field = fields[i];
var fieldType = this._type[field]; // type may be undefined
d[field] = util.convert(item[field], fieldType);
}
this._data[id] = d;
this.length++;
@ -840,7 +873,7 @@ DataSet.prototype._addItem = function (item) {
* @private
*/
DataSet.prototype._getItem = function (id, types) {
var field, value;
var field, value, i, len;
// get the item from the dataset
var raw = this._data[id];
@ -849,22 +882,22 @@ DataSet.prototype._getItem = function (id, types) {
}
// convert the items field types
var converted = {};
var converted = {},
fields = Object.keys(raw);
if (types) {
for (field in raw) {
if (raw.hasOwnProperty(field)) {
value = raw[field];
converted[field] = util.convert(value, types[field]);
}
for (i = 0, len = fields.length; i < len; i++) {
field = fields[i];
value = raw[field];
converted[field] = util.convert(value, types[field]);
}
}
else {
// no field types specified, no converting needed
for (field in raw) {
if (raw.hasOwnProperty(field)) {
value = raw[field];
converted[field] = value;
}
for (i = 0, len = fields.length; i < len; i++) {
field = fields[i];
value = raw[field];
converted[field] = value;
}
}
return converted;
@ -890,11 +923,11 @@ DataSet.prototype._updateItem = function (item) {
}
// merge with current item
for (var field in item) {
if (item.hasOwnProperty(field)) {
var fieldType = this._type[field]; // type may be undefined
d[field] = util.convert(item[field], fieldType);
}
var fields = Object.keys(item);
for (var i = 0, len = fields.length; i < len; i++) {
var field = fields[i];
var fieldType = this._type[field]; // type may be undefined
d[field] = util.convert(item[field], fieldType);
}
return id;

+ 55
- 17
lib/DataView.js View File

@ -35,7 +35,7 @@ function DataView (data, options) {
* @param {DataSet | DataView} data
*/
DataView.prototype.setData = function (data) {
var ids, i, len;
var ids, id, i, len;
if (this._data) {
// unsubscribe from current dataset
@ -44,12 +44,7 @@ DataView.prototype.setData = function (data) {
}
// trigger a remove of all items in memory
ids = [];
for (var id in this._ids) {
if (this._ids.hasOwnProperty(id)) {
ids.push(id);
}
}
ids = Object.keys(this._ids);
this._ids = {};
this.length = 0;
this._trigger('remove', {items: ids});
@ -84,34 +79,34 @@ DataView.prototype.setData = function (data) {
* containing a variable parameter.
*/
DataView.prototype.refresh = function () {
var id;
var id, i, len;
var ids = this._data.getIds({filter: this._options && this._options.filter});
var oldIds = Object.keys(this._ids);
var newIds = {};
var added = [];
var removed = [];
// check for additions
for (var i = 0; i < ids.length; i++) {
for (i = 0, len = ids.length; i < len; i++) {
id = ids[i];
newIds[id] = true;
if (!this._ids[id]) {
added.push(id);
this._ids[id] = true;
this.length++;
}
}
// check for removals
for (id in this._ids) {
if (this._ids.hasOwnProperty(id)) {
if (!newIds[id]) {
removed.push(id);
delete this._ids[id];
this.length--;
}
for (i = 0, len = oldIds.length; i < len; i++) {
id = oldIds[i];
if (!newIds[id]) {
removed.push(id);
delete this._ids[id];
}
}
this.length += added.length - removed.length;
// trigger events
if (added.length) {
this._trigger('add', {items: added});
@ -235,6 +230,49 @@ DataView.prototype.getIds = function (options) {
return ids;
};
/**
* Map every item in the dataset.
* @param {function} callback
* @param {Object} [options] Available options:
* {Object.<String, String>} [type]
* {String[]} [fields] filter fields
* {function} [filter] filter items
* {String | function} [order] Order the items by
* a field name or custom sort function.
* @return {Object[]} mappedItems
*/
DataView.prototype.map = function (callback,options) {
var mappedItems = [];
if (this._data) {
var defaultFilter = this._options.filter;
var filter;
if (options && options.filter) {
if (defaultFilter) {
filter = function (item) {
return defaultFilter(item) && options.filter(item);
}
}
else {
filter = options.filter;
}
}
else {
filter = defaultFilter;
}
mappedItems = this._data.map(callback,{
filter: filter,
order: options && options.order
});
}
else {
mappedItems = [];
}
return mappedItems;
};
/**
* Get the DataSet to which this DataView is connected. In case there is a chain
* of multiple DataViews, the root DataSet of this chain is returned.

+ 9
- 5
lib/graph3d/Graph3d.js View File

@ -96,6 +96,8 @@ function Graph3d(container, data, options) {
strokeWidth: 1 // px
};
this.dotSizeRatio = 0.02; // size of the dots as a fraction of the graph width
// create a frame and canvas
this.create();
@ -844,6 +846,8 @@ Graph3d.prototype.setOptions = function (options) {
if (options.yValueLabel !== undefined) this.yValueLabel = options.yValueLabel;
if (options.zValueLabel !== undefined) this.zValueLabel = options.zValueLabel;
if (options.dotSizeRatio !== undefined) this.dotSizeRatio = options.dotSizeRatio;
if (options.style !== undefined) {
var styleNumber = this._getStyleNumber(options.style);
if (styleNumber !== -1) {
@ -976,7 +980,7 @@ Graph3d.prototype._redrawLegend = function() {
if (this.style === Graph3d.STYLE.DOTCOLOR ||
this.style === Graph3d.STYLE.DOTSIZE) {
var dotSize = this.frame.clientWidth * 0.02;
var dotSize = this.frame.clientWidth * this.dotSizeRatio;
var widthMin, widthMax;
if (this.style === Graph3d.STYLE.DOTSIZE) {
@ -1613,7 +1617,7 @@ Graph3d.prototype._redrawDataDot = function() {
this.dataPoints.sort(sortDepth);
// draw the datapoints as colored circles
var dotSize = this.frame.clientWidth * 0.02; // px
var dotSize = this.frame.clientWidth * this.dotSizeRatio; // px
for (i = 0; i < this.dataPoints.length; i++) {
var point = this.dataPoints[i];
@ -2233,9 +2237,9 @@ Graph3d.prototype._showTooltip = function (dataPoint) {
}
else {
content.innerHTML = '<table>' +
'<tr><td>x:</td><td>' + dataPoint.point.x + '</td></tr>' +
'<tr><td>y:</td><td>' + dataPoint.point.y + '</td></tr>' +
'<tr><td>z:</td><td>' + dataPoint.point.z + '</td></tr>' +
'<tr><td>' + this.xLabel + ':</td><td>' + dataPoint.point.x + '</td></tr>' +
'<tr><td>' + this.yLabel + ':</td><td>' + dataPoint.point.y + '</td></tr>' +
'<tr><td>' + this.zLabel + ':</td><td>' + dataPoint.point.z + '</td></tr>' +
'</table>';
}

+ 19
- 21
lib/hammerUtil.js View File

@ -7,23 +7,14 @@ var Hammer = require('./module/hammer');
*/
exports.onTouch = function (hammer, callback) {
callback.inputHandler = function (event) {
if (event.isFirst && !isTouching) {
if (event.isFirst) {
callback(event);
isTouching = true;
setTimeout(function () {
isTouching = false;
}, 0);
}
};
hammer.on('hammer.input', callback.inputHandler);
};
// isTouching is true while a touch action is being emitted
// this is a hack to prevent `touch` from being fired twice
var isTouching = false;
/**
* Register a release event, taking place after a gesture
* @param {Hammer} hammer A hammer instance
@ -31,13 +22,8 @@ var isTouching = false;
*/
exports.onRelease = function (hammer, callback) {
callback.inputHandler = function (event) {
if (event.isFinal && !isReleasing) {
if (event.isFinal) {
callback(event);
isReleasing = true;
setTimeout(function () {
isReleasing = false;
}, 0);
}
};
@ -45,11 +31,6 @@ exports.onRelease = function (hammer, callback) {
};
// isReleasing is true while a release action is being emitted
// this is a hack to prevent `release` from being fired twice
var isReleasing = false;
/**
* Unregister a touch event, taking place before a gesture
* @param {Hammer} hammer A hammer instance
@ -65,3 +46,20 @@ exports.offTouch = function (hammer, callback) {
* @param {function} callback Callback, called as callback(event)
*/
exports.offRelease = exports.offTouch;
/**
* Hack the PinchRecognizer such that it doesn't prevent default behavior
* for vertical panning.
* @param {Hammer.Pinch} pinchRecognizer
* @return {Hammer.Pinch} returns the pinchRecognizer
*/
exports.disablePreventDefaultVertically = function (pinchRecognizer) {
var TOUCH_ACTION_PAN_Y = 'pan-y';
pinchRecognizer.getTouchAction = function() {
// default method returns [TOUCH_ACTION_NONE]
return [TOUCH_ACTION_PAN_Y];
};
return pinchRecognizer;
};

+ 1
- 1
lib/header.js View File

@ -8,7 +8,7 @@
* @date @@date
*
* @license
* Copyright (C) 2011-2015 Almende B.V, http://almende.com
* Copyright (C) 2011-2016 Almende B.V, http://almende.com
*
* Vis.js is dual licensed under both
*

+ 7
- 5
lib/network/Network.js View File

@ -144,7 +144,6 @@ Emitter(Network.prototype);
*/
Network.prototype.setOptions = function (options) {
if (options !== undefined) {
let errorFound = Validator.validate(options, allOptions);
if (errorFound === true) {
console.log('%cErrors have been found in the supplied options object.', printStyle);
@ -247,7 +246,7 @@ Network.prototype._updateVisibleIndices = function () {
for (let nodeId in nodes) {
if (nodes.hasOwnProperty(nodeId)) {
if (nodes[nodeId].options.hidden === false) {
this.body.nodeIndices.push(nodeId);
this.body.nodeIndices.push(nodes[nodeId].id);
}
}
}
@ -255,7 +254,7 @@ Network.prototype._updateVisibleIndices = function () {
for (let edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
if (edges[edgeId].options.hidden === false) {
this.body.edgeIndices.push(edgeId);
this.body.edgeIndices.push(edges[edgeId].id);
}
}
}
@ -270,7 +269,6 @@ Network.prototype.bindEventListeners = function () {
this.body.emitter.on("_dataChanged", () => {
// update shortcut lists
this._updateVisibleIndices();
this.physics.updatePhysicsData();
this.body.emitter.emit("_requestRedraw");
// call the dataUpdated event because the only difference between the two is the updating of the indices
this.body.emitter.emit("_dataUpdated");
@ -467,6 +465,7 @@ Network.prototype.startSimulation = function() {return this.physics.startSim
Network.prototype.stopSimulation = function() {return this.physics.stopSimulation.apply(this.physics,arguments);};
Network.prototype.stabilize = function() {return this.physics.stabilize.apply(this.physics,arguments);};
Network.prototype.getSelection = function() {return this.selectionHandler.getSelection.apply(this.selectionHandler,arguments);};
Network.prototype.setSelection = function() {return this.selectionHandler.setSelection.apply(this.selectionHandler,arguments);};
Network.prototype.getSelectedNodes = function() {return this.selectionHandler.getSelectedNodes.apply(this.selectionHandler,arguments);};
Network.prototype.getSelectedEdges = function() {return this.selectionHandler.getSelectedEdges.apply(this.selectionHandler,arguments);};
Network.prototype.getNodeAt = function() {
@ -485,7 +484,10 @@ Network.prototype.getEdgeAt = function() {
};
Network.prototype.selectNodes = function() {return this.selectionHandler.selectNodes.apply(this.selectionHandler,arguments);};
Network.prototype.selectEdges = function() {return this.selectionHandler.selectEdges.apply(this.selectionHandler,arguments);};
Network.prototype.unselectAll = function() {return this.selectionHandler.unselectAll.apply(this.selectionHandler,arguments);};
Network.prototype.unselectAll = function() {
this.selectionHandler.unselectAll.apply(this.selectionHandler,arguments);
this.redraw();
};
Network.prototype.redraw = function() {return this.renderer.redraw.apply(this.renderer,arguments);};
Network.prototype.getScale = function() {return this.view.getScale.apply(this.view,arguments);};
Network.prototype.getViewPosition = function() {return this.view.getViewPosition.apply(this.view,arguments);};

+ 96
- 0
lib/network/NetworkUtil.js View File

@ -0,0 +1,96 @@
let util = require("../util");
class NetworkUtil {
constructor() {}
/**
* Find the center position of the network considering the bounding boxes
*/
static getRange(allNodes, specificNodes = []) {
var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
if (specificNodes.length > 0) {
for (var i = 0; i < specificNodes.length; i++) {
node = allNodes[specificNodes[i]];
if (minX > node.shape.boundingBox.left) {
minX = node.shape.boundingBox.left;
}
if (maxX < node.shape.boundingBox.right) {
maxX = node.shape.boundingBox.right;
}
if (minY > node.shape.boundingBox.top) {
minY = node.shape.boundingBox.top;
} // top is negative, bottom is positive
if (maxY < node.shape.boundingBox.bottom) {
maxY = node.shape.boundingBox.bottom;
} // top is negative, bottom is positive
}
}
if (minX === 1e9 && maxX === -1e9 && minY === 1e9 && maxY === -1e9) {
minY = 0, maxY = 0, minX = 0, maxX = 0;
}
return {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
}
/**
* Find the center position of the network
*/
static getRangeCore(allNodes, specificNodes = []) {
var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
if (specificNodes.length > 0) {
for (var i = 0; i < specificNodes.length; i++) {
node = allNodes[specificNodes[i]];
if (minX > node.x) {
minX = node.x;
}
if (maxX < node.x) {
maxX = node.x;
}
if (minY > node.y) {
minY = node.y;
} // top is negative, bottom is positive
if (maxY < node.y) {
maxY = node.y;
} // top is negative, bottom is positive
}
}
if (minX === 1e9 && maxX === -1e9 && minY === 1e9 && maxY === -1e9) {
minY = 0, maxY = 0, minX = 0, maxX = 0;
}
return {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
}
/**
* @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
* @returns {{x: number, y: number}}
*/
static findCenter(range) {
return {x: (0.5 * (range.maxX + range.minX)),
y: (0.5 * (range.maxY + range.minY))};
}
/**
* This returns a clone of the options or options of the edge or node to be used for construction of new edges or check functions for new nodes.
* @param item
* @param type
* @returns {{}}
*/
static cloneOptions(item, type) {
let clonedOptions = {};
if (type === undefined || type === 'node') {
util.deepExtend(clonedOptions, item.options, true);
clonedOptions.x = item.x;
clonedOptions.y = item.y;
clonedOptions.amountOfConnections = item.edges.length;
}
else {
util.deepExtend(clonedOptions, item.options, true);
}
return clonedOptions;
}
}
export default NetworkUtil;

+ 8
- 5
lib/network/css/network-colorpicker.css View File

@ -1,14 +1,17 @@
div.vis-color-picker {
position:absolute;
top: 0px;
left: 30px;
margin-top:-140px;
margin-left:30px;
width:293px;
height:425px;
width:310px;
height:444px;
z-index: 1;
padding: 10px;
border-radius:15px;
background-color:#ffffff;
display:none;
display: none;
box-shadow: rgba(0,0,0,0.5) 0px 0px 10px 0px;
}
@ -18,8 +21,8 @@ div.vis-color-picker div.vis-arrow {
left:5px;
}
div.vis-color-picker div.vis-arrow:after,
div.vis-color-picker div.vis-arrow:before {
div.vis-color-picker div.vis-arrow::after,
div.vis-color-picker div.vis-arrow::before {
right: 100%;
top: 50%;
border: solid transparent;

+ 9
- 8
lib/network/css/network-manipulation.css View File

@ -12,17 +12,18 @@ div.vis-network div.vis-manipulation {
background: linear-gradient(to bottom, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* W3C */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#fcfcfc',GradientType=0 ); /* IE6-9 */
padding-top:4px;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 30px;
height: 28px;
}
div.vis-network div.vis-edit-mode {
position:absolute;
left: 0;
top: 15px;
top: 5px;
height: 30px;
}
@ -53,8 +54,7 @@ div.vis-network div.vis-close:hover {
div.vis-network div.vis-manipulation div.vis-button,
div.vis-network div.vis-edit-mode div.vis-button {
position:relative;
top:-7px;
float:left;
font-family: verdana;
font-size: 12px;
-moz-border-radius: 15px;
@ -63,8 +63,8 @@ div.vis-network div.vis-edit-mode div.vis-button {
background-position: 0px 0px;
background-repeat:no-repeat;
height:24px;
margin: 0px 0px 0px 10px;
vertical-align:middle;
margin-left: 10px;
/*vertical-align:middle;*/
cursor: pointer;
padding: 0px 8px 0px 8px;
-webkit-touch-callout: none;
@ -130,11 +130,12 @@ div.vis-network div.vis-edit-mode div.vis-label {
line-height: 25px;
}
div.vis-network div.vis-manipulation div.vis-separator-line {
float:left;
display:inline-block;
width:1px;
height:20px;
height:21px;
background-color: #bdbdbd;
margin: 5px 7px 0 15px;
margin: 0px 7px 0 15px; /*top right bottom left*/
}
/* TODO: is this redundant?

+ 1
- 1
lib/network/css/network-tooltip.css View File

@ -6,7 +6,7 @@ div.vis-network-tooltip {
font-family: verdana;
font-size:14px;
font-color:#000000;
color:#000000;
background-color: #f5f4ed;
-moz-border-radius: 3px;

+ 60
- 13
lib/network/modules/Canvas.js View File

@ -17,6 +17,7 @@ class Canvas {
this.resizeTimer = undefined;
this.resizeFunction = this._onResize.bind(this);
this.cameraState = {};
this.initialized = false;
this.options = {};
this.defaultOptions = {
@ -87,10 +88,16 @@ class Canvas {
* Get and store the cameraState
* @private
*/
_getCameraState() {
this.cameraState.previousWidth = this.frame.canvas.width;
this.cameraState.scale = this.body.view.scale;
this.cameraState.position = this.DOMtoCanvas({x: 0.5 * this.frame.canvas.width, y: 0.5 * this.frame.canvas.height});
_getCameraState(pixelRatio = this.pixelRatio) {
if (this.initialized === true) {
this.cameraState.previousWidth = this.frame.canvas.width / pixelRatio;
this.cameraState.previousHeight = this.frame.canvas.height / pixelRatio;
this.cameraState.scale = this.body.view.scale;
this.cameraState.position = this.DOMtoCanvas({
x: 0.5 * this.frame.canvas.width / pixelRatio,
y: 0.5 * this.frame.canvas.height / pixelRatio
});
}
}
/**
@ -98,17 +105,36 @@ class Canvas {
* @private
*/
_setCameraState() {
if (this.cameraState.scale !== undefined) {
this.body.view.scale = this.body.view.scale * (this.frame.canvas.clientWidth / this.cameraState.previousWidth);
if (this.cameraState.scale !== undefined &&
this.frame.canvas.clientWidth !== 0 &&
this.frame.canvas.clientHeight !== 0 &&
this.pixelRatio !== 0 &&
this.cameraState.previousWidth > 0) {
let widthRatio = (this.frame.canvas.width / this.pixelRatio) / this.cameraState.previousWidth;
let heightRatio = (this.frame.canvas.height / this.pixelRatio) / this.cameraState.previousHeight;
let newScale = this.cameraState.scale;
if (widthRatio != 1 && heightRatio != 1) {
newScale = this.cameraState.scale * 0.5 * (widthRatio + heightRatio);
}
else if (widthRatio != 1) {
newScale = this.cameraState.scale * widthRatio;
}
else if (heightRatio != 1) {
newScale = this.cameraState.scale * heightRatio;
}
this.body.view.scale = newScale;
// this comes from the view module.
var viewCenter = this.DOMtoCanvas({
var currentViewCenter = this.DOMtoCanvas({
x: 0.5 * this.frame.canvas.clientWidth,
y: 0.5 * this.frame.canvas.clientHeight
});
var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node
x: viewCenter.x - this.cameraState.position.x,
y: viewCenter.y - this.cameraState.position.y
x: currentViewCenter.x - this.cameraState.position.x,
y: currentViewCenter.y - this.cameraState.position.y
};
this.body.view.translation.x += distanceFromCenter.x * this.body.view.scale;
this.body.view.translation.y += distanceFromCenter.y * this.body.view.scale;
@ -127,7 +153,7 @@ class Canvas {
return value + 'px';
}
}
throw new Error('Could not use the value supplie for width or height:' + value);
throw new Error('Could not use the value supplied for width or height:' + value);
}
@ -196,7 +222,7 @@ class Canvas {
this.hammer = new Hammer(this.frame.canvas);
this.hammer.get('pinch').set({enable: true});
// enable to get better response, todo: test on mobile.
this.hammer.get('pan').set({threshold:5, direction:30}); // 30 is ALL_DIRECTIONS in hammer.
this.hammer.get('pan').set({threshold:5, direction: Hammer.DIRECTION_ALL});
hammerUtil.onTouch(this.hammer, (event) => {this.body.eventListeners.onTouch(event)});
this.hammer.on('tap', (event) => {this.body.eventListeners.onTap(event)});
@ -227,7 +253,6 @@ class Canvas {
* or '30%')
*/
setSize(width = this.options.width, height = this.options.height) {
this._getCameraState();
width = this._prepareValue(width);
height= this._prepareValue(height);
@ -235,7 +260,18 @@ class Canvas {
let oldWidth = this.frame.canvas.width;
let oldHeight = this.frame.canvas.height;
// 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);
if (width != this.options.width || height != this.options.height || this.frame.style.width != width || this.frame.style.height != height) {
this._getCameraState(previousRatio);
this.frame.style.width = width;
this.frame.style.height = height;
@ -254,6 +290,11 @@ class Canvas {
// this would adapt the width of the canvas to the width from 100% if and only if
// there is a change.
// 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)) {
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);
emitEvent = true;
@ -271,8 +312,14 @@ class Canvas {
oldWidth: Math.round(oldWidth / this.pixelRatio),
oldHeight: Math.round(oldHeight / this.pixelRatio)
});
// restore the camera on change.
this._setCameraState();
}
this._setCameraState();
// set initialized so the get and set camera will work from now on.
this.initialized = true;
return emitEvent;
};

+ 12
- 31
lib/network/modules/CanvasRenderer.js View File

@ -147,13 +147,11 @@ class CanvasRenderer {
this.canvas.setSize();
}
if (this.pixelRatio === undefined) {
this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1);
}
this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1);
ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0);
@ -162,6 +160,11 @@ class CanvasRenderer {
let h = this.canvas.frame.canvas.clientHeight;
ctx.clearRect(0, 0, w, h);
// if the div is hidden, we stop the redraw here for performance.
if (this.canvas.frame.clientWidth === 0) {
return;
}
// set scaling and translation
ctx.save();
ctx.translate(this.body.view.translation.x, this.body.view.translation.y);
@ -181,17 +184,13 @@ class CanvasRenderer {
this._drawNodes(ctx, hidden);
}
if (this.controlNodesActive === true) {
this._drawControlNodes(ctx);
}
ctx.beginPath();
//this.physics.nodesSolver._debug(ctx,"#F00F0F");
this.body.emitter.emit("afterDrawing", ctx);
ctx.closePath();
// restore original scaling and translation
ctx.restore();
if (hidden === true) {
ctx.clearRect(0, 0, w, h);
}
@ -256,7 +255,6 @@ class CanvasRenderer {
});
let viewableArea = {top:topLeft.y,left:topLeft.x,bottom:bottomRight.y,right:bottomRight.x};
// draw unselected nodes;
for (let i = 0; i < nodeIndices.length; i++) {
node = nodes[nodeIndices[i]];
@ -304,23 +302,6 @@ class CanvasRenderer {
}
}
/**
* Redraw all edges
* The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
* @param {CanvasRenderingContext2D} ctx
* @private
*/
_drawControlNodes(ctx) {
let edges = this.body.edges;
let edgeIndices = this.body.edgeIndices;
let edge;
for (let i = 0; i < edgeIndices.length; i++) {
edge = edges[edgeIndices[i]];
edge._drawControlNodes(ctx);
}
}
/**
* Determine if the browser requires a setTimeout or a requestAnimationFrame. This was required because
* some implementations (safari and IE9) did not support requestAnimationFrame

+ 86
- 89
lib/network/modules/Clustering.js View File

@ -1,16 +1,18 @@
let util = require("../../util");
import NetworkUtil from '../NetworkUtil';
import Cluster from './components/nodes/Cluster'
class ClusterEngine {
constructor(body) {
this.body = body;
this.clusteredNodes = {};
this.clusteredEdges = {};
this.options = {};
this.defaultOptions = {};
util.extend(this.options, this.defaultOptions);
this.body.emitter.on('_resetData', () => {this.clusteredNodes = {};})
this.body.emitter.on('_resetData', () => {this.clusteredNodes = {}; this.clusteredEdges = {};})
}
setOptions(options) {
@ -67,14 +69,14 @@ class ClusterEngine {
for (let i = 0; i < this.body.nodeIndices.length; i++) {
let nodeId = this.body.nodeIndices[i];
let node = this.body.nodes[nodeId];
let clonedOptions = this._cloneOptions(node);
let clonedOptions = NetworkUtil.cloneOptions(node);
if (options.joinCondition(clonedOptions) === true) {
childNodesObj[nodeId] = this.body.nodes[nodeId];
// collect the nodes that will be in the cluster
for (let i = 0; i < node.edges.length; i++) {
let edge = node.edges[i];
if (edge.hiddenByCluster !== true) {
if (this.clusteredEdges[edge.id] === undefined) {
childEdgesObj[edge.id] = edge;
}
}
@ -95,7 +97,7 @@ class ClusterEngine {
options = this._checkOptions(options);
let clusters = [];
let usedNodes = {};
let edge, edges, node, nodeId, visibleEdges;
let edge, edges, node, nodeId, relevantEdgeCount;
// collect the nodes that will be in the cluster
for (let i = 0; i < this.body.nodeIndices.length; i++) {
let childNodesObj = {};
@ -104,50 +106,45 @@ class ClusterEngine {
// if this node is already used in another cluster this session, we do not have to re-evaluate it.
if (usedNodes[nodeId] === undefined) {
visibleEdges = 0;
relevantEdgeCount = 0;
node = this.body.nodes[nodeId];
edges = [];
for (let j = 0; j < node.edges.length; j++) {
edge = node.edges[j];
if (edge.hiddenByCluster !== true) {
if (this.clusteredEdges[edge.id] === undefined) {
if (edge.toId !== edge.fromId) {
relevantEdgeCount++;
}
edges.push(edge);
}
}
// this node qualifies, we collect its neighbours to start the clustering process.
if (edges.length === edgeCount) {
if (relevantEdgeCount === edgeCount) {
let gatheringSuccessful = true;
for (let j = 0; j < edges.length; j++) {
edge = edges[j];
let childNodeId = this._getConnectedId(edge, nodeId);
// if unused and if not referencing itself
if (childNodeId !== nodeId && usedNodes[nodeId] === undefined) {
// add the nodes to the list by the join condition.
if (options.joinCondition === undefined) {
// add the nodes to the list by the join condition.
if (options.joinCondition === undefined) {
childEdgesObj[edge.id] = edge;
childNodesObj[nodeId] = this.body.nodes[nodeId];
childNodesObj[childNodeId] = this.body.nodes[childNodeId];
usedNodes[nodeId] = true;
}
else {
let clonedOptions = NetworkUtil.cloneOptions(this.body.nodes[nodeId]);
if (options.joinCondition(clonedOptions) === true) {
childEdgesObj[edge.id] = edge;
childNodesObj[nodeId] = this.body.nodes[nodeId];
childNodesObj[childNodeId] = this.body.nodes[childNodeId];
usedNodes[nodeId] = true;
}
else {
let clonedOptions = this._cloneOptions(this.body.nodes[nodeId]);
if (options.joinCondition(clonedOptions) === true) {
childEdgesObj[edge.id] = edge;
childNodesObj[nodeId] = this.body.nodes[nodeId];
usedNodes[nodeId] = true;
}
else {
// this node does not qualify after all.
gatheringSuccessful = false;
break;
}
// this node does not qualify after all.
gatheringSuccessful = false;
break;
}
}
else {
// this node does not qualify after all.
gatheringSuccessful = false;
break;
}
}
// add to the cluster queue
@ -212,16 +209,16 @@ class ClusterEngine {
let childNodesObj = {};
let childEdgesObj = {};
let parentNodeId = node.id;
let parentClonedOptions = this._cloneOptions(node);
let parentClonedOptions = NetworkUtil.cloneOptions(node);
childNodesObj[parentNodeId] = node;
// collect the nodes that will be in the cluster
for (let i = 0; i < node.edges.length; i++) {
let edge = node.edges[i];
if (edge.hiddenByCluster !== true) {
if (this.clusteredEdges[edge.id] === undefined) {
let childNodeId = this._getConnectedId(edge, parentNodeId);
// if the child node is not in a cluster (may not be needed now with the edge.hiddenByCluster check)
// if the child node is not in a cluster
if (this.clusteredNodes[childNodeId] === undefined) {
if (childNodeId !== parentNodeId) {
if (options.joinCondition === undefined) {
@ -230,7 +227,7 @@ class ClusterEngine {
}
else {
// clone the options and insert some additional parameters that could be interesting.
let childClonedOptions = this._cloneOptions(this.body.nodes[childNodeId]);
let childClonedOptions = NetworkUtil.cloneOptions(this.body.nodes[childNodeId]);
if (options.joinCondition(parentClonedOptions, childClonedOptions) === true) {
childEdgesObj[edge.id] = edge;
childNodesObj[childNodeId] = this.body.nodes[childNodeId];
@ -249,38 +246,17 @@ class ClusterEngine {
}
/**
* This returns a clone of the options or options of the edge or node to be used for construction of new edges or check functions for new nodes.
* @param objId
* @param type
* @returns {{}}
* @private
*/
_cloneOptions(item, type) {
let clonedOptions = {};
if (type === undefined || type === 'node') {
util.deepExtend(clonedOptions, item.options, true);
clonedOptions.x = item.x;
clonedOptions.y = item.y;
clonedOptions.amountOfConnections = item.edges.length;
}
else {
util.deepExtend(clonedOptions, item.options, true);
}
return clonedOptions;
}
/**
* This function creates the edges that will be attached to the cluster
* It looks for edges that are connected to the nodes from the "outside' of the cluster.
*
* @param childNodesObj
* @param newEdges
* @param options
* @param childEdgesObj
* @param clusterNodeProperties
* @param clusterEdgeProperties
* @private
*/
_createClusterEdges (childNodesObj, clusterNodeProperties, clusterEdgeProperties) {
_createClusterEdges (childNodesObj, childEdgesObj, clusterNodeProperties, clusterEdgeProperties) {
let edge, childNodeId, childNode, toId, fromId, otherNodeId;
// loop over all child nodes and their edges to find edges going out of the cluster
@ -295,17 +271,23 @@ class ClusterEngine {
for (let j = 0; j < childNode.edges.length; j++) {
edge = childNode.edges[j];
// we only handle edges that are visible to the system, not the disabled ones from the clustering process.
if (edge.hiddenByCluster !== true) {
// set up the from and to.
if (edge.toId == childNodeId) { // this is a double equals because ints and strings can be interchanged here.
toId = clusterNodeProperties.id;
fromId = edge.fromId;
otherNodeId = fromId;
if (this.clusteredEdges[edge.id] === undefined) {
// self-referencing edges will be added to the "hidden" list
if (edge.toId == edge.fromId) {
childEdgesObj[edge.id] = edge;
}
else {
toId = edge.toId;
fromId = clusterNodeProperties.id;
otherNodeId = toId;
// set up the from and to.
if (edge.toId == childNodeId) { // this is a double equals because ints and strings can be interchanged here.
toId = clusterNodeProperties.id;
fromId = edge.fromId;
otherNodeId = fromId;
}
else {
toId = edge.toId;
fromId = clusterNodeProperties.id;
otherNodeId = toId;
}
}
// Only edges from the cluster outwards are being replaced.
@ -321,7 +303,7 @@ class ClusterEngine {
for (let j = 0; j < createEdges.length; j++) {
let edge = createEdges[j].edge;
// copy the options of the edge we will replace
let clonedOptions = this._cloneOptions(edge, 'edge');
let clonedOptions = NetworkUtil.cloneOptions(edge, 'edge');
// make sure the properties of clusterEdges are superimposed on it
util.deepExtend(clonedOptions, clusterEdgeProperties);
@ -340,8 +322,8 @@ class ClusterEngine {
newEdge.connect();
// hide the replaced edge
this._backupEdgeOptions(edge);
edge.setOptions({physics:false, hidden:true});
edge.hiddenByCluster = true;
}
}
@ -369,7 +351,7 @@ class ClusterEngine {
* @private
*/
_cluster(childNodesObj, childEdgesObj, options, refreshData = true) {
// kill condition: no children so can't cluster or only one node in the cluster, dont bother
// kill condition: no children so can't cluster or only one node in the cluster, don't bother
if (Object.keys(childNodesObj).length < 2) {return;}
// check if this cluster call is not trying to cluster anything that is in another cluster.
@ -389,18 +371,18 @@ class ClusterEngine {
let childNodesOptions = [];
for (let nodeId in childNodesObj) {
if (childNodesObj.hasOwnProperty(nodeId)) {
let clonedOptions = this._cloneOptions(childNodesObj[nodeId]);
let clonedOptions = NetworkUtil.cloneOptions(childNodesObj[nodeId]);
childNodesOptions.push(clonedOptions);
}
}
// get clusterproperties based on childNodes
// get cluster properties based on childNodes
let childEdgesOptions = [];
for (let edgeId in childEdgesObj) {
if (childEdgesObj.hasOwnProperty(edgeId)) {
// these cluster edges will be removed on creation of the cluster.
if (edgeId.substr(0, 12) !== "clusterEdge:") {
let clonedOptions = this._cloneOptions(childEdgesObj[edgeId], 'edge');
let clonedOptions = NetworkUtil.cloneOptions(childEdgesObj[edgeId], 'edge');
childEdgesOptions.push(clonedOptions);
}
}
@ -421,7 +403,7 @@ class ClusterEngine {
}
// give the clusterNode a postion if it does not have one.
// give the clusterNode a position if it does not have one.
let pos = undefined;
if (clusterNodeProperties.x === undefined) {
pos = this._getClusterPosition(childNodesObj);
@ -446,16 +428,18 @@ class ClusterEngine {
// finally put the cluster node into global
this.body.nodes[clusterNodeProperties.id] = clusterNode;
// create the new edges that will connect to the cluster
this._createClusterEdges(childNodesObj, clusterNodeProperties, options.clusterEdgeProperties);
// create the new edges that will connect to the cluster, all self-referencing edges will be added to childEdgesObject here.
this._createClusterEdges(childNodesObj, childEdgesObj, clusterNodeProperties, options.clusterEdgeProperties);
// disable the childEdges
for (let edgeId in childEdgesObj) {
if (childEdgesObj.hasOwnProperty(edgeId)) {
if (this.body.edges[edgeId] !== undefined) {
let edge = this.body.edges[edgeId];
// cache the options before changing
this._backupEdgeOptions(edge);
// disable physics and hide the edge
edge.setOptions({physics:false, hidden:true});
edge.hiddenByCluster = true;
}
}
}
@ -477,6 +461,20 @@ class ClusterEngine {
}
}
_backupEdgeOptions(edge) {
if (this.clusteredEdges[edge.id] === undefined) {
this.clusteredEdges[edge.id] = {physics: edge.options.physics, hidden: edge.options.hidden};
}
}
_restoreEdge(edge) {
let originalOptions = this.clusteredEdges[edge.id];
if (originalOptions !== undefined) {
edge.setOptions({physics: originalOptions.physics, hidden: originalOptions.hidden});
delete this.clusteredEdges[edge.id];
}
}
/**
* Check if a node is a cluster.
@ -566,8 +564,8 @@ class ClusterEngine {
let containedNode = this.body.nodes[nodeId];
containedNode = containedNodes[nodeId];
// inherit position
containedNode.x = clusterNode.x;
containedNode.y = clusterNode.y;
if (containedNode.options.fixed.x === false) {containedNode.x = clusterNode.x;}
if (containedNode.options.fixed.y === false) {containedNode.y = clusterNode.y;}
}
}
}
@ -581,7 +579,7 @@ class ClusterEngine {
containedNode.vx = clusterNode.vx;
containedNode.vy = clusterNode.vy;
// we use these methods to avoid reinstantiating the shape, which happens with setOptions.
// we use these methods to avoid re-instantiating the shape, which happens with setOptions.
containedNode.setOptions({hidden:false, physics:true});
delete this.clusteredNodes[nodeId];
@ -622,7 +620,7 @@ class ClusterEngine {
}
// clone the options and apply the cluster options to them
let clonedOptions = this._cloneOptions(transferEdge, 'edge');
let clonedOptions = NetworkUtil.cloneOptions(transferEdge, 'edge');
util.deepExtend(clonedOptions, otherCluster.clusterEdgeProperties);
// apply the edge specific options to it.
@ -639,8 +637,7 @@ class ClusterEngine {
else {
let replacedEdge = this.body.edges[edge.clusteringEdgeReplacingId];
if (replacedEdge !== undefined) {
replacedEdge.setOptions({physics: true, hidden: false});
replacedEdge.hiddenByCluster = false;
this._restoreEdge(replacedEdge);
}
}
edge.cleanup();
@ -652,8 +649,7 @@ class ClusterEngine {
// handle the releasing of the edges
for (let edgeId in containedEdges) {
if (containedEdges.hasOwnProperty(edgeId)) {
let edge = containedEdges[edgeId];
edge.setOptions({physics: true, hidden: false});
this._restoreEdge(containedEdges[edgeId]);
}
}
@ -666,12 +662,12 @@ class ClusterEngine {
}
getNodesInCluster(clusterId) {
let nodesArray = []
let nodesArray = [];
if (this.isCluster(clusterId) === true) {
let containedNodes = this.body.nodes[clusterId].containedNodes;
for (let nodeId in containedNodes) {
if (containedNodes.hasOwnProperty(nodeId)) {
nodesArray.push(nodeId)
nodesArray.push(this.body.nodes[nodeId].id)
}
}
}
@ -683,7 +679,6 @@ class ClusterEngine {
* Get the stack clusterId's that a certain node resides in. cluster A -> cluster B -> cluster C -> node
* @param nodeId
* @returns {Array}
* @private
*/
findNode(nodeId) {
let stack = [];
@ -691,11 +686,13 @@ class ClusterEngine {
let counter = 0;
while (this.clusteredNodes[nodeId] !== undefined && counter < max) {
stack.push(this.clusteredNodes[nodeId].node);
stack.push(this.body.nodes[nodeId].id);
nodeId = this.clusteredNodes[nodeId].clusterId;
counter++;
}
stack.push(this.body.nodes[nodeId]);
stack.push(this.body.nodes[nodeId].id);
stack.reverse();
return stack;
}

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

@ -27,6 +27,7 @@ class EdgesHandler {
middle: {enabled: false, scaleFactor:1},
from: {enabled: false, scaleFactor:1}
},
arrowStrikethrough: true,
color: {
color:'#848484',
highlight:'#848484',
@ -74,6 +75,7 @@ class EdgesHandler {
selfReferenceSize:20,
shadow:{
enabled: false,
color: 'rgba(0,0,0,0.5)',
size:10,
x:5,
y:5
@ -106,7 +108,7 @@ class EdgesHandler {
let edge = this.body.edges[edgeId];
let edgeData = this.body.data.edges._data[edgeId];
// only forcilby remove the smooth curve if the data has been set of the edge has the smooth curves defined.
// only forcibly remove the smooth curve if the data has been set of the edge has the smooth curves defined.
// this is because a change in the global would not affect these curves.
if (edgeData !== undefined) {
let edgeOptions = edgeData.smooth;
@ -139,6 +141,10 @@ class EdgesHandler {
this.body.emitter.on("refreshEdges", this.refresh.bind(this));
this.body.emitter.on("refresh", this.refresh.bind(this));
this.body.emitter.on("destroy", () => {
util.forEach(this.edgesListeners, (callback, event) => {
if (this.body.data.edges)
this.body.data.edges.off(event, callback);
});
delete this.body.functions.createEdge;
delete this.edgesListeners.add;
delete this.edgesListeners.update;
@ -153,7 +159,7 @@ class EdgesHandler {
// use the parser from the Edge class to fill in all shorthand notations
Edge.parseOptions(this.options, options);
// hanlde multiple input cases for color
// handle multiple input cases for color
if (options.color !== undefined) {
this.markAllEdgesAsDirty();
}

+ 49
- 44
lib/network/modules/InteractionHandler.js View File

@ -189,7 +189,7 @@ class InteractionHandler {
let selectedNodesCount = this.selectionHandler._getSelectedNodeCount();
let currentSelection = this.selectionHandler.getSelection();
let {nodesChanges, edgesChanges} = this._determineIfDifferent(previousSelection, currentSelection);
let {nodesChanged, edgesChanged} = this._determineIfDifferent(previousSelection, currentSelection);
let nodeSelected = false;
if (selectedNodesCount - previouslySelectedNodeCount > 0) { // node was selected
@ -197,32 +197,34 @@ class InteractionHandler {
selected = true;
nodeSelected = true;
}
else if (selectedNodesCount - previouslySelectedNodeCount < 0) { // node was deselected
else if (nodesChanged === true && selectedNodesCount > 0) {
this.selectionHandler._generateClickEvent('deselectNode', event, pointer, previousSelection);
this.selectionHandler._generateClickEvent('selectNode', event, pointer);
nodeSelected = true;
selected = true;
}
else if (selectedNodesCount === previouslySelectedNodeCount && nodesChanges === true) {
else if (selectedNodesCount - previouslySelectedNodeCount < 0) { // node was deselected
this.selectionHandler._generateClickEvent('deselectNode', event, pointer, previousSelection);
this.selectionHandler._generateClickEvent('selectNode', event, pointer);
nodeSelected = true;
selected = true;
}
// handle the selected edges
if (selectedEdgesCount - previouslySelectedEdgeCount > 0 && nodeSelected === false) { // edge was selected
this.selectionHandler._generateClickEvent('selectEdge', event, pointer);
selected = true;
}
else if (selectedEdgesCount - previouslySelectedEdgeCount < 0) { // edge was deselected
else if (selectedEdgesCount > 0 && edgesChanged === true) {
this.selectionHandler._generateClickEvent('deselectEdge', event, pointer, previousSelection);
this.selectionHandler._generateClickEvent('selectEdge', event, pointer);
selected = true;
}
else if (selectedEdgesCount === previouslySelectedEdgeCount && edgesChanges === true) {
else if (selectedEdgesCount - previouslySelectedEdgeCount < 0) { // edge was deselected
this.selectionHandler._generateClickEvent('deselectEdge', event, pointer, previousSelection);
this.selectionHandler._generateClickEvent('selectEdge', event, pointer);
selected = true;
}
// fire the select event if anything has been selected or deselected
if (selected === true) { // select or unselect
this.selectionHandler._generateClickEvent('select', event, pointer);
@ -234,35 +236,35 @@ class InteractionHandler {
* This function checks if the nodes and edges previously selected have changed.
* @param previousSelection
* @param currentSelection
* @returns {{nodesChanges: boolean, edgesChanges: boolean}}
* @returns {{nodesChanged: boolean, edgesChanged: boolean}}
* @private
*/
_determineIfDifferent(previousSelection,currentSelection) {
let nodesChanges = false;
let edgesChanges = false;
let nodesChanged = false;
let edgesChanged = false;
for (let i = 0; i < previousSelection.nodes.length; i++) {
if (currentSelection.nodes.indexOf(previousSelection.nodes[i]) === -1) {
nodesChanges = true;
nodesChanged = true;
}
}
for (let i = 0; i < currentSelection.nodes.length; i++) {
if (previousSelection.nodes.indexOf(previousSelection.nodes[i]) === -1) {
nodesChanges = true;
nodesChanged = true;
}
}
for (let i = 0; i < previousSelection.edges.length; i++) {
if (currentSelection.edges.indexOf(previousSelection.edges[i]) === -1) {
edgesChanges = true;
edgesChanged = true;
}
}
for (let i = 0; i < currentSelection.edges.length; i++) {
if (previousSelection.edges.indexOf(previousSelection.edges[i]) === -1) {
edgesChanges = true;
edgesChanged = true;
}
}
return {nodesChanges, edgesChanges};
return {nodesChanged, edgesChanged};
}
@ -487,38 +489,41 @@ class InteractionHandler {
* @private
*/
onMouseWheel(event) {
// retrieve delta
let delta = 0;
if (event.wheelDelta) { /* IE/Opera. */
delta = event.wheelDelta / 120;
} else if (event.detail) { /* Mozilla case. */
// In Mozilla, sign of delta is different than in IE.
// Also, delta is multiple of 3.
delta = -event.detail / 3;
}
// If delta is nonzero, handle it.
// Basically, delta is now positive if wheel was scrolled up,
// and negative, if wheel was scrolled down.
if (delta !== 0) {
// calculate the new scale
let scale = this.body.view.scale;
let zoom = delta / 10;
if (delta < 0) {
zoom = zoom / (1 - zoom);
if (this.options.zoomView === true) {
// retrieve delta
let delta = 0;
if (event.wheelDelta) { /* IE/Opera. */
delta = event.wheelDelta / 120;
}
else if (event.detail) { /* Mozilla case. */
// In Mozilla, sign of delta is different than in IE.
// Also, delta is multiple of 3.
delta = -event.detail / 3;
}
scale *= (1 + zoom);
// calculate the pointer location
let pointer = this.getPointer({x:event.clientX, y:event.clientY});
// If delta is nonzero, handle it.
// Basically, delta is now positive if wheel was scrolled up,
// and negative, if wheel was scrolled down.
if (delta !== 0) {
// apply the new scale
this.zoom(scale, pointer);
}
// calculate the new scale
let scale = this.body.view.scale;
let zoom = delta / 10;
if (delta < 0) {
zoom = zoom / (1 - zoom);
}
scale *= (1 + zoom);
// calculate the pointer location
let pointer = this.getPointer({x: event.clientX, y: event.clientY});
// Prevent default actions caused by mouse wheel.
event.preventDefault();
// apply the new scale
this.zoom(scale, pointer);
}
// Prevent default actions caused by mouse wheel.
event.preventDefault();
}
}

+ 0
- 4
lib/network/modules/KamadaKawai.js View File

@ -1,7 +1,3 @@
/**
* Created by Alex on 8/7/2015.
*/
// distance finding algorithm
import FloydWarshall from "./components/algorithms/FloydWarshall.js"

+ 937
- 144
lib/network/modules/LayoutEngine.js
File diff suppressed because it is too large
View File


+ 24
- 23
lib/network/modules/ManipulationSystem.js View File

@ -110,6 +110,7 @@ class ManipulationSystem {
}
}
enableEditMode() {
this.editMode = true;
@ -143,7 +144,7 @@ class ManipulationSystem {
// restore the state of any bound functions or events, remove control nodes, restore physics
this._clean();
// reset global letiables
// reset global variables
this.manipulationDOM = {};
// if the gui is enabled, draw all elements.
@ -221,8 +222,6 @@ class ManipulationSystem {
/**
* Create the toolbar for adding Nodes
*
* @private
*/
addNodeMode() {
// when using the gui, enable edit mode if it wasnt already.
@ -250,8 +249,6 @@ class ManipulationSystem {
/**
* call the bound function to handle the editing of the node. The node has to be selected.
*
* @private
*/
editNode() {
// when using the gui, enable edit mode if it wasnt already.
@ -266,7 +263,7 @@ class ManipulationSystem {
this.inMode = 'editNode';
if (typeof this.options.editNode === 'function') {
if (node.isCluster !== true) {
let data = util.deepExtend({}, node.options, true);
let data = util.deepExtend({}, node.options, false);
data.x = node.x;
data.y = node.y;
@ -298,8 +295,6 @@ class ManipulationSystem {
/**
* create the toolbar to connect nodes
*
* @private
*/
addEdgeMode() {
// when using the gui, enable edit mode if it wasnt already.
@ -334,11 +329,9 @@ class ManipulationSystem {
/**
* create the toolbar to edit edges
*
* @private
*/
editEdgeMode() {
// when using the gui, enable edit mode if it wasnt already.
// when using the gui, enable edit mode if it wasn't already.
if (this.editMode !== true) {
this.enableEditMode();
}
@ -376,15 +369,12 @@ class ManipulationSystem {
// temporarily overload UI functions, cleaned up automatically because of _temporaryBindUI
this._temporaryBindUI('onTouch', this._controlNodeTouch.bind(this)); // used to get the position
this._temporaryBindUI('onTap', () => {
}); // disabled
this._temporaryBindUI('onHold', () => {
}); // disabled
this._temporaryBindUI('onTap', () => {}); // disabled
this._temporaryBindUI('onHold', () => {}); // disabled
this._temporaryBindUI('onDragStart', this._controlNodeDragStart.bind(this));// used to select control node
this._temporaryBindUI('onDrag', this._controlNodeDrag.bind(this)); // used to drag control node
this._temporaryBindUI('onDragEnd', this._controlNodeDragEnd.bind(this)); // used to connect or revert control nodes
this._temporaryBindUI('onMouseMove', () => {
}); // disabled
this._temporaryBindUI('onMouseMove', () => {}); // disabled
// create function to position control nodes correctly on movement
// automatically cleaned up because we use the temporary bind
@ -409,8 +399,6 @@ class ManipulationSystem {
/**
* delete everything in the selection
*
* @private
*/
deleteSelected() {
// when using the gui, enable edit mode if it wasnt already.
@ -560,7 +548,11 @@ class ManipulationSystem {
controlNodeStyle.x = x;
controlNodeStyle.y = y;
return this.body.functions.createNode(controlNodeStyle);
// we have to define the bounding box in order for the nodes to be drawn immediately
let node = this.body.functions.createNode(controlNodeStyle);
node.shape.boundingBox = {left: x, right:x, top:y, bottom:y};
return node;
}
@ -650,7 +642,7 @@ class ManipulationSystem {
// remove the manipulation divs
if (this.manipulationDiv) {this.canvas.frame.removeChild(this.manipulationDiv);}
if (this.editModeDiv) {this.canvas.frame.removeChild(this.editModeDiv);}
if (this.closeDiv) {this.canvas.frame.removeChild(this.manipulationDiv);}
if (this.closeDiv) {this.canvas.frame.removeChild(this.closeDiv);}
// set the references to undefined
this.manipulationDiv = undefined;
@ -856,6 +848,11 @@ class ManipulationSystem {
edge.edgeType.to = to;
}
// we use the selection to find the node that is being dragged. We explicitly select it here.
if (this.selectedControlNode !== undefined) {
this.selectionHandler.selectObject(this.selectedControlNode)
}
this.body.emitter.emit('_redraw');
}
@ -868,7 +865,6 @@ class ManipulationSystem {
this.body.emitter.emit('disablePhysics');
let pointer = this.body.functions.getPointer(event.center);
let pos = this.canvas.DOMtoCanvas(pointer);
if (this.selectedControlNode !== undefined) {
this.selectedControlNode.x = pos.x;
this.selectedControlNode.y = pos.y;
@ -892,7 +888,13 @@ class ManipulationSystem {
let pointer = this.body.functions.getPointer(event.center);
let pointerObj = this.selectionHandler._pointerToPositionObject(pointer);
let edge = this.body.edges[this.edgeBeingEditedId];
// if the node that was dragged is not a control node, return
if (this.selectedControlNode === undefined) {
return;
}
// we use the selection to find the node that is being dragged. We explicitly DEselect the control node here.
this.selectionHandler.unselectAll();
let overlappingNodeIds = this.selectionHandler._getAllNodesOverlappingWith(pointerObj);
let node = undefined;
for (let i = overlappingNodeIds.length-1; i >= 0; i--) {
@ -901,7 +903,6 @@ class ManipulationSystem {
break;
}
}
// perform the connection
if (node !== undefined && this.selectedControlNode !== undefined) {
if (node.isCluster === true) {

+ 11
- 4
lib/network/modules/NodesHandler.js View File

@ -87,6 +87,7 @@ class NodesHandler {
},
shadow: {
enabled: false,
color: 'rgba(0,0,0,0.5)',
size: 10,
x: 5,
y: 5
@ -95,7 +96,9 @@ class NodesHandler {
shapeProperties: {
borderDashes: false, // only for borders
borderRadius: 6, // only for box shape
useImageSize: false // only for image and circularImage shapes
interpolation: true, // only for image and circularImage shapes
useImageSize: false, // only for image and circularImage shapes
useBorderWithImage: false // only for image shape
},
size: 25,
title: undefined,
@ -113,6 +116,10 @@ class NodesHandler {
this.body.emitter.on('refreshNodes', this.refresh.bind(this));
this.body.emitter.on('refresh', this.refresh.bind(this));
this.body.emitter.on('destroy', () => {
util.forEach(this.nodesListeners, (callback, event) => {
if (this.body.data.nodes)
this.body.data.nodes.off(event, callback);
});
delete this.body.functions.createNode;
delete this.nodesListeners.add;
delete this.nodesListeners.update;
@ -355,7 +362,7 @@ class NodesHandler {
if (dataset._data.hasOwnProperty(nodeId)) {
let node = this.body.nodes[nodeId];
if (dataset._data[nodeId].x != Math.round(node.x) || dataset._data[nodeId].y != Math.round(node.y)) {
dataArray.push({ id: nodeId, x: Math.round(node.x), y: Math.round(node.y) });
dataArray.push({ id: node.id, x: Math.round(node.x), y: Math.round(node.y) });
}
}
}
@ -386,13 +393,13 @@ class NodesHandler {
let nodeObj = {}; // used to quickly check if node already exists
for (let i = 0; i < node.edges.length; i++) {
let edge = node.edges[i];
if (edge.toId == nodeId) { // these are double equals since ids can be numeric or string
if (edge.toId == node.id) { // these are double equals since ids can be numeric or string
if (nodeObj[edge.fromId] === undefined) {
nodeList.push(edge.fromId);
nodeObj[edge.fromId] = true;
}
}
else if (edge.fromId == nodeId) { // these are double equals since ids can be numeric or string
else if (edge.fromId == node.id) { // these are double equals since ids can be numeric or string
if (nodeObj[edge.toId] === undefined) {
nodeList.push(edge.toId);
nodeObj[edge.toId] = true;

+ 60
- 17
lib/network/modules/PhysicsEngine.js View File

@ -86,12 +86,14 @@ class PhysicsEngine {
};
util.extend(this.options, this.defaultOptions);
this.timestep = 0.5;
this.layoutFailed = false;
this.bindEventListeners();
}
bindEventListeners() {
this.body.emitter.on('initPhysics', () => {this.initPhysics();});
this.body.emitter.on('_layoutFailed', () => {this.layoutFailed = true;});
this.body.emitter.on('resetPhysics', () => {this.stopSimulation(); this.ready = false;});
this.body.emitter.on('disablePhysics', () => {this.physicsEnabled = false; this.stopSimulation();});
this.body.emitter.on('restorePhysics', () => {
@ -110,6 +112,14 @@ class PhysicsEngine {
this.stopSimulation(false);
this.body.emitter.off();
});
// this event will trigger a rebuilding of the cache everything. Used when nodes or edges have been added or removed.
this.body.emitter.on("_dataChanged", () => {
// update shortcut lists
this.updatePhysicsData();
});
// debug: show forces
// this.body.emitter.on("afterDrawing", (ctx) => {this._drawForces(ctx);});
}
@ -127,7 +137,7 @@ class PhysicsEngine {
else {
this.physicsEnabled = true;
util.selectiveNotDeepExtend(['stabilization'], this.options, options);
util.mergeOptions(this.options, options, 'stabilization')
util.mergeOptions(this.options, options, 'stabilization');
if (options.enabled === undefined) {
this.options.enabled = true;
@ -191,7 +201,7 @@ class PhysicsEngine {
else {
this.stabilized = false;
this.ready = true;
this.body.emitter.emit('fit', {}, false);
this.body.emitter.emit('fit', {}, this.layoutFailed); // if the layout failed, we use the approximation for the zoom
this.startSimulation();
}
}
@ -244,7 +254,7 @@ class PhysicsEngine {
/**
* The viewFunction inserts this step into each renderloop. It calls the physics tick and handles the cleanup at stabilized.
* The viewFunction inserts this step into each render loop. It calls the physics tick and handles the cleanup at stabilized.
*
*/
simulationStep() {
@ -271,10 +281,11 @@ class PhysicsEngine {
* trigger the stabilized event.
* @private
*/
_emitStabilized() {
if (this.stabilizationIterations > 1) {
_emitStabilized(amountOfIterations = this.stabilizationIterations) {
if (this.stabilizationIterations > 1 || this.startedStabilization === true) {
setTimeout(() => {
this.body.emitter.emit('stabilized', {iterations: this.stabilizationIterations});
this.body.emitter.emit('stabilized', {iterations: amountOfIterations});
this.startedStabilization = false;
this.stabilizationIterations = 0;
}, 0);
}
@ -286,6 +297,12 @@ class PhysicsEngine {
* @private
*/
physicsTick() {
// this is here to ensure that there is no start event when the network is already stable.
if (this.startedStabilization === false) {
this.body.emitter.emit('startStabilizing');
this.startedStabilization = true;
}
if (this.stabilized === false) {
// adaptivity means the timestep adapts to the situation, only applicable for stabilization
if (this.adaptiveTimestep === true && this.adaptiveTimestepEnabled === true) {
@ -348,13 +365,6 @@ class PhysicsEngine {
if (this.stabilized === true) {
this.revert();
}
else {
// this is here to ensure that there is no start event when the network is already stable.
if (this.startedStabilization === false) {
this.body.emitter.emit('startStabilizing');
this.startedStabilization = true;
}
}
this.stabilizationIterations++;
}
@ -376,7 +386,7 @@ class PhysicsEngine {
for (let nodeId in nodes) {
if (nodes.hasOwnProperty(nodeId)) {
if (nodes[nodeId].options.physics === true) {
this.physicsBody.physicsNodeIndices.push(nodeId);
this.physicsBody.physicsNodeIndices.push(nodes[nodeId].id);
}
}
}
@ -385,7 +395,7 @@ class PhysicsEngine {
for (let edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
if (edges[edgeId].options.physics === true) {
this.physicsBody.physicsEdgeIndices.push(edgeId);
this.physicsBody.physicsEdgeIndices.push(edges[edgeId].id);
}
}
}
@ -463,7 +473,7 @@ class PhysicsEngine {
}
/**
* move the nodes one timestap and check if they are stabilized
* move the nodes one timestep and check if they are stabilized
* @returns {boolean}
*/
moveNodes() {
@ -585,7 +595,6 @@ class PhysicsEngine {
/**
* Find a stable position for all nodes
* @private
*/
stabilize(iterations = this.options.stabilization.iterations) {
if (typeof iterations !== 'number') {
@ -629,6 +638,12 @@ class PhysicsEngine {
* @private
*/
_stabilizationBatch() {
// this is here to ensure that there is at least one start event.
if (this.startedStabilization === false) {
this.body.emitter.emit('startStabilizing');
this.startedStabilization = true;
}
var count = 0;
while (this.stabilized === false && count < this.options.stabilization.updateInterval && this.stabilizationIterations < this.targetIterations) {
this.physicsTick();
@ -671,6 +686,34 @@ class PhysicsEngine {
this.ready = true;
}
_drawForces(ctx) {
for (var i = 0; i < this.physicsBody.physicsNodeIndices.length; i++) {
let node = this.body.nodes[this.physicsBody.physicsNodeIndices[i]];
let force = this.physicsBody.forces[this.physicsBody.physicsNodeIndices[i]];
let factor = 20;
let colorFactor = 0.03;
let forceSize = Math.sqrt(Math.pow(force.x,2) + Math.pow(force.x,2));
let size = Math.min(Math.max(5,forceSize),15);
let arrowSize = 3*size;
let color = util.HSVToHex((180 - Math.min(1,Math.max(0,colorFactor*forceSize))*180) / 360,1,1);
ctx.lineWidth = size;
ctx.strokeStyle = color;
ctx.beginPath();
ctx.moveTo(node.x,node.y);
ctx.lineTo(node.x+factor*force.x, node.y+factor*force.y);
ctx.stroke();
let angle = Math.atan2(force.y, force.x);
ctx.fillStyle = color;
ctx.arrow(node.x + factor*force.x + Math.cos(angle)*arrowSize, node.y + factor*force.y+Math.sin(angle)*arrowSize, angle, arrowSize);
ctx.fill();
}
}
}

+ 49
- 39
lib/network/modules/SelectionHandler.js View File

@ -159,7 +159,6 @@ class SelectionHandler {
*
* @param {{x: Number, y: Number}} pointer
* @return {Node | undefined} node
* @private
*/
getNodeAt(pointer, returnNode = true) {
// we first check if this is an navigation controls element
@ -217,7 +216,6 @@ class SelectionHandler {
*
* @param pointer
* @returns {undefined}
* @private
*/
getEdgeAt(pointer, returnEdge = true) {
let positionObject = this._pointerToPositionObject(pointer);
@ -277,6 +275,7 @@ class SelectionHandler {
_removeFromSelection(obj) {
if (obj instanceof Node) {
delete this.selectionObj.nodes[obj.id];
this._unselectConnectedEdges(obj);
}
else {
delete this.selectionObj.edges[obj.id];
@ -285,8 +284,6 @@ class SelectionHandler {
/**
* Unselect all. The selectionObj is useful for this.
*
* @private
*/
unselectAll() {
for(let nodeId in this.selectionObj.nodes) {
@ -299,7 +296,7 @@ class SelectionHandler {
this.selectionObj.edges[edgeId].unselect();
}
}
this.selectionObj = {nodes:{},edges:{}};
}
@ -575,7 +572,7 @@ class SelectionHandler {
if (this.options.selectable === true) {
for (let nodeId in this.selectionObj.nodes) {
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
idArray.push(nodeId);
idArray.push(this.selectionObj.nodes[nodeId].id);
}
}
}
@ -593,13 +590,54 @@ class SelectionHandler {
if (this.options.selectable === true) {
for (let edgeId in this.selectionObj.edges) {
if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
idArray.push(edgeId);
idArray.push(this.selectionObj.edges[edgeId].id);
}
}
}
return idArray;
}
/**
* Updates the current selection
* @param {{nodes: Array.<String>, edges: Array.<String>}} Selection
* @param {Object} options Options
*/
setSelection(selection, options = {}) {
let i, id;
if (!selection || (!selection.nodes && !selection.edges))
throw 'Selection must be an object with nodes and/or edges properties';
// first unselect any selected node, if option is true or undefined
if (options.unselectAll || options.unselectAll === undefined) {
this.unselectAll();
}
if (selection.nodes) {
for (i = 0; i < selection.nodes.length; i++) {
id = selection.nodes[i];
let node = this.body.nodes[id];
if (!node) {
throw new RangeError('Node with id "' + id + '" not found');
}
// don't select edges with it
this.selectObject(node, options.highlightEdges);
}
}
if (selection.edges) {
for (i = 0; i < selection.edges.length; i++) {
id = selection.edges[i];
let edge = this.body.edges[id];
if (!edge) {
throw new RangeError('Edge with id "' + id + '" not found');
}
this.selectObject(edge);
}
}
this.body.emitter.emit('_requestRedraw');
}
/**
* select zero or more nodes with the option to highlight edges
@ -608,24 +646,10 @@ class SelectionHandler {
* @param {boolean} [highlightEdges]
*/
selectNodes(selection, highlightEdges = true) {
let i, id;
if (!selection || (selection.length === undefined))
throw 'Selection must be an array with ids';
// first unselect any selected node
this.unselectAll();
for (i = 0; i < selection.length; i++) {
id = selection[i];
let node = this.body.nodes[id];
if (!node) {
throw new RangeError('Node with id "' + id + '" not found');
}
this.selectObject(node,highlightEdges);
}
this.body.emitter.emit('_requestRedraw');
this.setSelection({nodes: selection}, {highlightEdges: highlightEdges});
}
@ -635,24 +659,10 @@ class SelectionHandler {
* selected nodes.
*/
selectEdges(selection) {
let i, id;
if (!selection || (selection.length === undefined))
throw 'Selection must be an array with ids';
// first unselect any selected objects
this.unselectAll();
for (i = 0; i < selection.length; i++) {
id = selection[i];
let edge = this.body.edges[id];
if (!edge) {
throw new RangeError('Edge with id "' + id + '" not found');
}
this.selectObject(edge);
}
this.body.emitter.emit('_requestRedraw');
this.setSelection({edges: selection});
}
/**

+ 31
- 84
lib/network/modules/View.js View File

@ -1,4 +1,6 @@
var util = require('../../util');
let util = require('../../util');
import NetworkUtil from '../NetworkUtil';
class View {
constructor(body, canvas) {
@ -29,80 +31,25 @@ class View {
}
/**
* Find the center position of the network
* @private
*/
_getRange(specificNodes = []) {
var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
if (specificNodes.length > 0) {
for (var i = 0; i < specificNodes.length; i++) {
node = this.body.nodes[specificNodes[i]];
if (minX > (node.shape.boundingBox.left)) {
minX = node.shape.boundingBox.left;
}
if (maxX < (node.shape.boundingBox.right)) {
maxX = node.shape.boundingBox.right;
}
if (minY > (node.shape.boundingBox.top)) {
minY = node.shape.boundingBox.top;
} // top is negative, bottom is positive
if (maxY < (node.shape.boundingBox.bottom)) {
maxY = node.shape.boundingBox.bottom;
} // top is negative, bottom is positive
}
}
else {
for (var i = 0; i < this.body.nodeIndices.length; i++) {
node = this.body.nodes[this.body.nodeIndices[i]];
if (minX > (node.shape.boundingBox.left)) {
minX = node.shape.boundingBox.left;
}
if (maxX < (node.shape.boundingBox.right)) {
maxX = node.shape.boundingBox.right;
}
if (minY > (node.shape.boundingBox.top)) {
minY = node.shape.boundingBox.top;
} // top is negative, bottom is positive
if (maxY < (node.shape.boundingBox.bottom)) {
maxY = node.shape.boundingBox.bottom;
} // top is negative, bottom is positive
}
}
if (minX === 1e9 && maxX === -1e9 && minY === 1e9 && maxY === -1e9) {
minY = 0, maxY = 0, minX = 0, maxX = 0;
}
return {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
}
/**
* @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
* @returns {{x: number, y: number}}
* @private
*/
_findCenter(range) {
return {x: (0.5 * (range.maxX + range.minX)),
y: (0.5 * (range.maxY + range.minY))};
}
/**
* This function zooms out to fit all data on screen based on amount of nodes
* @param {Object} Options
* @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false;
*/
fit(options = {nodes:[]}, initialZoom = false) {
var range;
var zoomLevel;
let range;
let zoomLevel;
if (options.nodes === undefined || options.nodes.length === 0) {
options.nodes = this.body.nodeIndices;
}
if (initialZoom === true) {
// check if more than half of the nodes have a predefined position. If so, we use the range, not the approximation.
var positionDefined = 0;
for (var nodeId in this.body.nodes) {
let positionDefined = 0;
for (let nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
var node = this.body.nodes[nodeId];
let node = this.body.nodes[nodeId];
if (node.predefinedPosition === true) {
positionDefined += 1;
}
@ -113,24 +60,24 @@ class View {
return;
}
range = this._getRange(options.nodes);
range = NetworkUtil.getRange(this.body.nodes, options.nodes);
var numberOfNodes = this.body.nodeIndices.length;
let numberOfNodes = this.body.nodeIndices.length;
zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
// correct for larger canvasses.
var factor = Math.min(this.canvas.frame.canvas.clientWidth / 600, this.canvas.frame.canvas.clientHeight / 600);
let factor = Math.min(this.canvas.frame.canvas.clientWidth / 600, this.canvas.frame.canvas.clientHeight / 600);
zoomLevel *= factor;
}
else {
this.body.emitter.emit("_resizeNodes");
range = this._getRange(options.nodes);
range = NetworkUtil.getRange(this.body.nodes, options.nodes);
var xDistance = Math.abs(range.maxX - range.minX) * 1.1;
var yDistance = Math.abs(range.maxY - range.minY) * 1.1;
let xDistance = Math.abs(range.maxX - range.minX) * 1.1;
let yDistance = Math.abs(range.maxY - range.minY) * 1.1;
var xZoomLevel = this.canvas.frame.canvas.clientWidth / xDistance;
var yZoomLevel = this.canvas.frame.canvas.clientHeight / yDistance;
let xZoomLevel = this.canvas.frame.canvas.clientWidth / xDistance;
let yZoomLevel = this.canvas.frame.canvas.clientHeight / yDistance;
zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel;
}
@ -142,8 +89,8 @@ class View {
zoomLevel = 1.0;
}
var center = this._findCenter(range);
var animationOptions = {position: center, scale: zoomLevel, animation: options.animation};
let center = NetworkUtil.findCenter(range);
let animationOptions = {position: center, scale: zoomLevel, animation: options.animation};
this.moveTo(animationOptions);
}
@ -157,7 +104,7 @@ class View {
*/
focus(nodeId, options = {}) {
if (this.body.nodes[nodeId] !== undefined) {
var nodePosition = {x: this.body.nodes[nodeId].x, y: this.body.nodes[nodeId].y};
let nodePosition = {x: this.body.nodes[nodeId].x, y: this.body.nodes[nodeId].y};
options.position = nodePosition;
options.lockedOnNode = nodeId;
@ -229,9 +176,9 @@ class View {
// set the scale so the viewCenter is based on the correct zoom level. This is overridden in the transitionRedraw
// but at least then we'll have the target transition
this.body.view.scale = this.targetScale;
var viewCenter = this.canvas.DOMtoCanvas({x: 0.5 * this.canvas.frame.canvas.clientWidth, y: 0.5 * this.canvas.frame.canvas.clientHeight});
let viewCenter = this.canvas.DOMtoCanvas({x: 0.5 * this.canvas.frame.canvas.clientWidth, y: 0.5 * this.canvas.frame.canvas.clientHeight});
var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node
let distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node
x: viewCenter.x - options.position.x,
y: viewCenter.y - options.position.y
};
@ -268,14 +215,14 @@ class View {
* @private
*/
_lockedRedraw() {
var nodePosition = {x: this.body.nodes[this.lockedOnNodeId].x, y: this.body.nodes[this.lockedOnNodeId].y};
var viewCenter = this.canvas.DOMtoCanvas({x: 0.5 * this.canvas.frame.canvas.clientWidth, y: 0.5 * this.canvas.frame.canvas.clientHeight});
var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node
let nodePosition = {x: this.body.nodes[this.lockedOnNodeId].x, y: this.body.nodes[this.lockedOnNodeId].y};
let viewCenter = this.canvas.DOMtoCanvas({x: 0.5 * this.canvas.frame.canvas.clientWidth, y: 0.5 * this.canvas.frame.canvas.clientHeight});
let distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node
x: viewCenter.x - nodePosition.x,
y: viewCenter.y - nodePosition.y
};
var sourceTranslation = this.body.view.translation;
var targetTranslation = {
let sourceTranslation = this.body.view.translation;
let targetTranslation = {
x: sourceTranslation.x + distanceFromCenter.x * this.body.view.scale + this.lockedOnNodeOffset.x,
y: sourceTranslation.y + distanceFromCenter.y * this.body.view.scale + this.lockedOnNodeOffset.y
};
@ -300,7 +247,7 @@ class View {
this.easingTime += this.animationSpeed;
this.easingTime = finished === true ? 1.0 : this.easingTime;
var progress = util.easingFunctions[this.animationEasingFunction](this.easingTime);
let progress = util.easingFunctions[this.animationEasingFunction](this.easingTime);
this.body.view.scale = this.sourceScale + (this.targetScale - this.sourceScale) * progress;
this.body.view.translation = {

+ 59
- 26
lib/network/modules/components/Edge.js View File

@ -27,6 +27,7 @@ class Edge {
throw "No body provided";
}
this.options = util.bridgeObject(globalOptions);
this.globalOptions = globalOptions;
this.body = body;
// initialize variables
@ -65,7 +66,7 @@ class Edge {
}
this.colorDirty = true;
Edge.parseOptions(this.options, options, true);
Edge.parseOptions(this.options, options, true, this.globalOptions);
if (options.id !== undefined) {this.id = options.id;}
if (options.from !== undefined) {this.fromId = options.from;}
@ -92,8 +93,9 @@ class Edge {
return dataChanged;
}
static parseOptions(parentOptions, newOptions, allowDeletion = false) {
static parseOptions(parentOptions, newOptions, allowDeletion = false, globalOptions = {}) {
var fields = [
'arrowStrikethrough',
'id',
'from',
'hidden',
@ -104,6 +106,7 @@ class Edge {
'line',
'opacity',
'physics',
'scaling',
'selectionWidth',
'selfReferenceSize',
'to',
@ -115,29 +118,27 @@ class Edge {
// only deep extend the items in the field array. These do not have shorthand.
util.selectiveDeepExtend(fields, parentOptions, newOptions, allowDeletion);
util.mergeOptions(parentOptions, newOptions, 'smooth');
util.mergeOptions(parentOptions, newOptions, 'shadow');
util.mergeOptions(parentOptions, newOptions, 'smooth', allowDeletion, globalOptions);
util.mergeOptions(parentOptions, newOptions, 'shadow', allowDeletion, globalOptions);
if (newOptions.dashes !== undefined && newOptions.dashes !== null) {
parentOptions.dashes = newOptions.dashes;
}
else if (allowDeletion === true && newOptions.dashes === null) {
parentOptions.dashes = undefined;
delete parentOptions.dashes;
parentOptions.dashes = Object.create(globalOptions.dashes); // this sets the pointer of the option back to the global option.
}
// set the scaling newOptions
if (newOptions.scaling !== undefined && newOptions.scaling !== null) {
if (newOptions.scaling.min !== undefined) {parentOptions.scaling.min = newOptions.scaling.min;}
if (newOptions.scaling.max !== undefined) {parentOptions.scaling.max = newOptions.scaling.max;}
util.mergeOptions(parentOptions.scaling, newOptions.scaling, 'label');
util.mergeOptions(parentOptions.scaling, newOptions.scaling, 'label', allowDeletion, globalOptions.scaling);
}
else if (allowDeletion === true && newOptions.scaling === null) {
parentOptions.scaling = undefined;
delete parentOptions.scaling;
parentOptions.scaling = Object.create(globalOptions.scaling); // this sets the pointer of the option back to the global option.
}
// hanlde multiple input cases for arrows
// handle multiple input cases for arrows
if (newOptions.arrows !== undefined && newOptions.arrows !== null) {
if (typeof newOptions.arrows === 'string') {
let arrows = newOptions.arrows.toLowerCase();
@ -146,21 +147,22 @@ class Edge {
if (arrows.indexOf("from") != -1) {parentOptions.arrows.from.enabled = true;}
}
else if (typeof newOptions.arrows === 'object') {
util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'to');
util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'middle');
util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'from');
util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'to', allowDeletion, globalOptions.arrows);
util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'middle', allowDeletion, globalOptions.arrows);
util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'from', allowDeletion, globalOptions.arrows);
}
else {
throw new Error("The arrow newOptions can only be an object or a string. Refer to the documentation. You used:" + JSON.stringify(newOptions.arrows));
}
}
else if (allowDeletion === true && newOptions.arrows === null) {
parentOptions.arrows = undefined;
delete parentOptions.arrows;
parentOptions.arrows = Object.create(globalOptions.arrows); // this sets the pointer of the option back to the global option.
}
// hanlde multiple input cases for color
// handle multiple input cases for color
if (newOptions.color !== undefined && newOptions.color !== null) {
// make a copy of the parent object in case this is referring to the global one (due to object create once, then update)
parentOptions.color = util.deepExtend({}, parentOptions.color, true);
if (util.isString(newOptions.color)) {
parentOptions.color.color = newOptions.color;
parentOptions.color.highlight = newOptions.color;
@ -181,14 +183,16 @@ class Edge {
}
}
else if (allowDeletion === true && newOptions.color === null) {
parentOptions.color = undefined;
delete parentOptions.color;
parentOptions.color = util.bridgeObject(globalOptions.color); // set the object back to the global options
}
// handle the font settings
if (newOptions.font !== undefined) {
if (newOptions.font !== undefined && newOptions.font !== null) {
Label.parseOptions(parentOptions.font, newOptions);
}
else if (allowDeletion === true && newOptions.font === null) {
parentOptions.font = util.bridgeObject(globalOptions.font); // set the object back to the global options
}
}
@ -343,6 +347,7 @@ class Edge {
}
this._setInteractionWidths();
this.updateLabelModule();
}
_setInteractionWidths() {
@ -369,17 +374,45 @@ class Edge {
* @param {CanvasRenderingContext2D} ctx
*/
draw(ctx) {
let via = this.edgeType.drawLine(ctx, this.selected, this.hover);
this.drawArrows(ctx, via);
this.drawLabel (ctx, via);
// get the via node from the edge type
let viaNode = this.edgeType.getViaNode();
let arrowData = {};
// restore edge targets to defaults
this.edgeType.fromPoint = this.edgeType.from;
this.edgeType.toPoint = this.edgeType.to;
// from and to arrows give a different end point for edges. we set them here
if (this.options.arrows.from.enabled === true) {
arrowData.from = this.edgeType.getArrowData(ctx,'from', viaNode, this.selected, this.hover);
if (this.options.arrowStrikethrough === false)
this.edgeType.fromPoint = arrowData.from.core;
}
if (this.options.arrows.to.enabled === true) {
arrowData.to = this.edgeType.getArrowData(ctx,'to', viaNode, this.selected, this.hover);
if (this.options.arrowStrikethrough === false)
this.edgeType.toPoint = arrowData.to.core;
}
// the middle arrow depends on the line, which can depend on the to and from arrows so we do this one lastly.
if (this.options.arrows.middle.enabled === true) {
arrowData.middle = this.edgeType.getArrowData(ctx,'middle', viaNode, this.selected, this.hover);
}
// draw everything
this.edgeType.drawLine(ctx, this.selected, this.hover, viaNode);
this.drawArrows(ctx, arrowData);
this.drawLabel (ctx, viaNode);
}
drawArrows(ctx, viaNode) {
if (this.options.arrows.from.enabled === true) {this.edgeType.drawArrowHead(ctx,'from', viaNode, this.selected, this.hover);}
if (this.options.arrows.middle.enabled === true) {this.edgeType.drawArrowHead(ctx,'middle', viaNode, this.selected, this.hover);}
if (this.options.arrows.to.enabled === true) {this.edgeType.drawArrowHead(ctx,'to', viaNode, this.selected, this.hover);}
drawArrows(ctx, arrowData) {
if (this.options.arrows.from.enabled === true) {this.edgeType.drawArrowHead(ctx, this.selected, this.hover, arrowData.from);}
if (this.options.arrows.middle.enabled === true) {this.edgeType.drawArrowHead(ctx, this.selected, this.hover, arrowData.middle);}
if (this.options.arrows.to.enabled === true) {this.edgeType.drawArrowHead(ctx, this.selected, this.hover, arrowData.to);}
}
drawLabel(ctx, viaNode) {
if (this.options.label !== undefined) {
// set style

+ 18
- 13
lib/network/modules/components/Node.js View File

@ -48,6 +48,7 @@ import {printStyle} from "../../../shared/Validator";
class Node {
constructor(options, body, imagelist, grouplist, globalOptions) {
this.options = util.bridgeObject(globalOptions);
this.globalOptions = globalOptions;
this.body = body;
this.edges = []; // all edges connected to this node
@ -125,7 +126,6 @@ class Node {
if (options.size !== undefined) {this.baseSize = options.size;}
if (options.value !== undefined) {options.value = parseFloat(options.value);}
// copy group options
if (typeof options.group === 'number' || (typeof options.group === 'string' && options.group != '')) {
var groupObj = this.grouplist.get(options.group);
@ -135,7 +135,7 @@ class Node {
}
// this transforms all shorthands into fully defined options
Node.parseOptions(this.options, options, true);
Node.parseOptions(this.options, options, true, this.globalOptions);
// load the images
if (this.options.image !== undefined) {
@ -150,7 +150,6 @@ class Node {
this.updateLabelModule();
this.updateShape(currentShape);
if (options.hidden !== undefined || options.physics !== undefined) {
return true;
}
@ -163,8 +162,10 @@ class Node {
* Static so it can also be used by the handler.
* @param parentOptions
* @param newOptions
* @param allowDeletion
* @param globalOptions
*/
static parseOptions(parentOptions, newOptions, allowDeletion = false) {
static parseOptions(parentOptions, newOptions, allowDeletion = false, globalOptions = {}) {
var fields = [
'color',
'font',
@ -174,7 +175,7 @@ class Node {
util.selectiveNotDeepExtend(fields, parentOptions, newOptions, allowDeletion);
// merge the shadow options into the parent.
util.mergeOptions(parentOptions, newOptions, 'shadow');
util.mergeOptions(parentOptions, newOptions, 'shadow', allowDeletion, globalOptions);
// individual shape newOptions
if (newOptions.color !== undefined && newOptions.color !== null) {
@ -182,8 +183,7 @@ class Node {
util.fillIfDefined(parentOptions.color, parsedColor);
}
else if (allowDeletion === true && newOptions.color === null) {
parentOptions.color = undefined;
delete parentOptions.color;
parentOptions.color = util.bridgeObject(globalOptions.color); // set the object back to the global options
}
// handle the fixed options
@ -203,13 +203,16 @@ class Node {
}
// handle the font options
if (newOptions.font !== undefined) {
if (newOptions.font !== undefined && newOptions.font !== null) {
Label.parseOptions(parentOptions.font, newOptions);
}
else if (allowDeletion === true && newOptions.font === null) {
parentOptions.font = util.bridgeObject(globalOptions.font); // set the object back to the global options
}
// handle the scaling options, specifically the label part
if (newOptions.scaling !== undefined) {
util.mergeOptions(parentOptions.scaling, newOptions.scaling, 'label');
util.mergeOptions(parentOptions.scaling, newOptions.scaling, 'label', allowDeletion, globalOptions.scaling);
}
}
@ -378,6 +381,8 @@ class Node {
this.options.size = this.baseSize;
this.options.font.size = this.baseFontSize;
}
this.updateLabelModule();
}
@ -429,10 +434,10 @@ class Node {
*/
isBoundingBoxOverlappingWith(obj) {
return (
this.shape.boundingBox.left < obj.right &&
this.shape.boundingBox.right > obj.left &&
this.shape.boundingBox.top < obj.bottom &&
this.shape.boundingBox.bottom > obj.top
this.shape.boundingBox.left < obj.right &&
this.shape.boundingBox.right > obj.left &&
this.shape.boundingBox.top < obj.bottom &&
this.shape.boundingBox.bottom > obj.top
);
}
}

+ 5
- 2
lib/network/modules/components/algorithms/FloydWarshall.js View File

@ -23,8 +23,11 @@ class FloydWarshall {
// put the weights for the edges in. This assumes unidirectionality.
for (let i = 0; i < edgesArray.length; i++) {
let edge = edges[edgesArray[i]];
D_matrix[edge.fromId][edge.toId] = 1;
D_matrix[edge.toId][edge.fromId] = 1;
// edge has to be connected if it counts to the distances. If it is connected to inner clusters it will crash so we also check if it is in the D_matrix
if (edge.connected === true && D_matrix[edge.fromId] !== undefined && D_matrix[edge.toId] !== undefined) {
D_matrix[edge.fromId][edge.toId] = 1;
D_matrix[edge.toId][edge.fromId] = 1;
}
}
let nodeCount = nodesArray.length;

+ 30
- 10
lib/network/modules/components/edges/BezierEdgeDynamic.js View File

@ -9,16 +9,27 @@ class BezierEdgeDynamic extends BezierEdgeBase {
}
setOptions(options) {
// check if the physics has changed.
let physicsChange = false;
if (this.options.physics !== options.physics) {
physicsChange = true;
}
// set the options and the to and from nodes
this.options = options;
this.id = this.options.id;
this.from = this.body.nodes[this.options.from];
this.to = this.body.nodes[this.options.to];
// setup the support node and connect
this.setupSupportNode();
this.connect();
// when we change the physics state of the edge, we reposition the support node.
if (this.options.physics !== options.physics) {
if (physicsChange === true) {
this.via.setOptions({physics: this.options.physics})
this.positionBezierNode();
}
this.connect();
}
connect() {
@ -28,7 +39,7 @@ class BezierEdgeDynamic extends BezierEdgeBase {
this.via.setOptions({physics:false})
}
else {
// fix weird behaviour where a selfreferencing node has physics enabled
// fix weird behaviour where a self referencing node has physics enabled
if (this.from.id === this.to.id) {
this.via.setOptions({physics: false})
}
@ -91,15 +102,24 @@ class BezierEdgeDynamic extends BezierEdgeBase {
* @param {CanvasRenderingContext2D} ctx
* @private
*/
_line(ctx) {
_line(ctx, viaNode) {
// draw a straight line
ctx.beginPath();
ctx.moveTo(this.from.x, this.from.y);
ctx.quadraticCurveTo(this.via.x, this.via.y, this.to.x, this.to.y);
ctx.moveTo(this.fromPoint.x, this.fromPoint.y);
// fallback to normal straight edges
if (viaNode.x === undefined) {
ctx.lineTo(this.toPoint.x, this.toPoint.y);
}
else {
ctx.quadraticCurveTo(viaNode.x, viaNode.y, this.toPoint.x, this.toPoint.y);
}
// draw shadow if enabled
this.enableShadow(ctx);
ctx.stroke();
this.disableShadow(ctx);
}
getViaNode() {
return this.via;
}
@ -107,14 +127,14 @@ class BezierEdgeDynamic extends BezierEdgeBase {
/**
* Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way
* @param percentage
* @param via
* @param viaNode
* @returns {{x: number, y: number}}
* @private
*/
getPoint(percentage) {
getPoint(percentage, viaNode = this.via) {
let t = percentage;
let x = Math.pow(1 - t, 2) * this.from.x + (2 * t * (1 - t)) * this.via.x + Math.pow(t, 2) * this.to.x;
let y = Math.pow(1 - t, 2) * this.from.y + (2 * t * (1 - t)) * this.via.y + Math.pow(t, 2) * this.to.y;
let x = Math.pow(1 - t, 2) * this.fromPoint.x + (2 * t * (1 - t)) * viaNode.x + Math.pow(t, 2) * this.toPoint.x;
let y = Math.pow(1 - t, 2) * this.fromPoint.y + (2 * t * (1 - t)) * viaNode.y + Math.pow(t, 2) * this.toPoint.y;
return {x: x, y: y};
}

+ 21
- 15
lib/network/modules/components/edges/BezierEdgeStatic.js View File

@ -10,28 +10,34 @@ class BezierEdgeStatic extends BezierEdgeBase {
* @param {CanvasRenderingContext2D} ctx
* @private
*/
_line(ctx) {
_line(ctx, viaNode) {
// draw a straight line
ctx.beginPath();
ctx.moveTo(this.from.x, this.from.y);
let via = this._getViaCoordinates();
let returnValue = via;
ctx.moveTo(this.fromPoint.x, this.fromPoint.y);
// fallback to normal straight edges
if (via.x === undefined) {
ctx.lineTo(this.to.x, this.to.y);
returnValue = undefined;
if (viaNode.x === undefined) {
ctx.lineTo(this.toPoint.x, this.toPoint.y);
}
else {
ctx.quadraticCurveTo(via.x, via.y, this.to.x, this.to.y);
ctx.quadraticCurveTo(viaNode.x, viaNode.y, this.toPoint.x, this.toPoint.y);
}
// draw shadow if enabled
this.enableShadow(ctx);
ctx.stroke();
this.disableShadow(ctx);
return returnValue;
}
getViaNode() {
return this._getViaCoordinates();
}
/**
* We do not use the to and fromPoints here to make the via nodes the same as edges without arrows.
* @returns {{x: undefined, y: undefined}}
* @private
*/
_getViaCoordinates() {
let xVia = undefined;
let yVia = undefined;
@ -214,21 +220,21 @@ class BezierEdgeStatic extends BezierEdgeBase {
return this._findBorderPositionBezier(nearNode, ctx, options.via);
}
_getDistanceToEdge(x1, y1, x2, y2, x3, y3, via = this._getViaCoordinates()) { // x3,y3 is the point
return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, via);
_getDistanceToEdge(x1, y1, x2, y2, x3, y3, viaNode = this._getViaCoordinates()) { // x3,y3 is the point
return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, viaNode);
}
/**
* Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way
* @param percentage
* @param via
* @param viaNode
* @returns {{x: number, y: number}}
* @private
*/
getPoint(percentage, via = this._getViaCoordinates()) {
getPoint(percentage, viaNode = this._getViaCoordinates()) {
var t = percentage;
var x = Math.pow(1 - t, 2) * this.from.x + (2 * t * (1 - t)) * via.x + Math.pow(t, 2) * this.to.x;
var y = Math.pow(1 - t, 2) * this.from.y + (2 * t * (1 - t)) * via.y + Math.pow(t, 2) * this.to.y;
var x = Math.pow(1 - t, 2) * this.fromPoint.x + (2 * t * (1 - t)) * viaNode.x + Math.pow(t, 2) * this.toPoint.x;
var y = Math.pow(1 - t, 2) * this.fromPoint.y + (2 * t * (1 - t)) * viaNode.y + Math.pow(t, 2) * this.toPoint.y;
return {x: x, y: y};
}

+ 14
- 12
lib/network/modules/components/edges/CubicBezierEdge.js View File

@ -10,28 +10,26 @@ class CubicBezierEdge extends CubicBezierEdgeBase {
* @param {CanvasRenderingContext2D} ctx
* @private
*/
_line(ctx) {
_line(ctx, viaNodes) {
// get the coordinates of the support points.
let [via1,via2] = this._getViaCoordinates();
let returnValue = [via1,via2];
let via1 = viaNodes[0];
let via2 = viaNodes[1];
// start drawing the line.
ctx.beginPath();
ctx.moveTo(this.from.x, this.from.y);
ctx.moveTo(this.fromPoint.x, this.fromPoint.y);
// fallback to normal straight edges
if (via1.x === undefined) {
ctx.lineTo(this.to.x, this.to.y);
returnValue = undefined;
if (viaNodes === undefined || via1.x === undefined) {
ctx.lineTo(this.toPoint.x, this.toPoint.y);
}
else {
ctx.bezierCurveTo(via1.x, via1.y, via2.x, via2.y, this.to.x, this.to.y);
ctx.bezierCurveTo(via1.x, via1.y, via2.x, via2.y, this.toPoint.x, this.toPoint.y);
}
// draw shadow if enabled
this.enableShadow(ctx);
ctx.stroke();
this.disableShadow(ctx);
return returnValue;
}
_getViaCoordinates() {
@ -39,7 +37,7 @@ class CubicBezierEdge extends CubicBezierEdgeBase {
let dy = this.from.y - this.to.y;
let x1, y1, x2, y2;
let roundness = this.options.smooth.roundness;;
let roundness = this.options.smooth.roundness;
// horizontal if x > y or if direction is forced or if direction is horizontal
if ((Math.abs(dx) > Math.abs(dy) || this.options.smooth.forceDirection === true || this.options.smooth.forceDirection === 'horizontal') && this.options.smooth.forceDirection !== 'vertical') {
@ -58,6 +56,10 @@ class CubicBezierEdge extends CubicBezierEdgeBase {
return [{x: x1, y: y1},{x: x2, y: y2}];
}
getViaNode() {
return this._getViaCoordinates();
}
_findBorderPosition(nearNode, ctx) {
return this._findBorderPositionBezier(nearNode, ctx);
}
@ -80,8 +82,8 @@ class CubicBezierEdge extends CubicBezierEdgeBase {
vec[1] = 3 * t * Math.pow(1 - t, 2);
vec[2] = 3 * Math.pow(t,2) * (1 - t);
vec[3] = Math.pow(t, 3);
let x = vec[0] * this.from.x + vec[1] * via1.x + vec[2] * via2.x + vec[3] * this.to.x;
let y = vec[0] * this.from.y + vec[1] * via1.y + vec[2] * via2.y + vec[3] * this.to.y;
let x = vec[0] * this.fromPoint.x + vec[1] * via1.x + vec[2] * via2.x + vec[3] * this.toPoint.x;
let y = vec[0] * this.fromPoint.y + vec[1] * via1.y + vec[2] * via2.y + vec[3] * this.toPoint.y;
return {x: x, y: y};
}

+ 7
- 4
lib/network/modules/components/edges/StraightEdge.js View File

@ -13,12 +13,15 @@ class StraightEdge extends EdgeBase {
_line(ctx) {
// draw a straight line
ctx.beginPath();
ctx.moveTo(this.from.x, this.from.y);
ctx.lineTo(this.to.x, this.to.y);
ctx.moveTo(this.fromPoint.x, this.fromPoint.y);
ctx.lineTo(this.toPoint.x, this.toPoint.y);
// draw shadow if enabled
this.enableShadow(ctx);
ctx.stroke();
this.disableShadow(ctx);
}
getViaNode() {
return undefined;
}
@ -31,8 +34,8 @@ class StraightEdge extends EdgeBase {
*/
getPoint(percentage) {
return {
x: (1 - percentage) * this.from.x + percentage * this.to.x,
y: (1 - percentage) * this.from.y + percentage * this.to.y
x: (1 - percentage) * this.fromPoint.x + percentage * this.toPoint.x,
y: (1 - percentage) * this.fromPoint.y + percentage * this.toPoint.y
}
}

+ 55
- 54
lib/network/modules/components/edges/util/EdgeBase.js View File

@ -4,11 +4,14 @@ class EdgeBase {
constructor(options, body, labelModule) {
this.body = body;
this.labelModule = labelModule;
this.options = {};
this.setOptions(options);
this.colorDirty = true;
this.color = {};
this.selectionWidth = 2;
this.hoverWidth = 1.5;
this.fromPoint = this.from;
this.toPoint = this.to;
}
connect() {
@ -31,36 +34,32 @@ class EdgeBase {
* @param {CanvasRenderingContext2D} ctx
* @private
*/
drawLine(ctx, selected, hover) {
drawLine(ctx, selected, hover, viaNode) {
// set style
ctx.strokeStyle = this.getColor(ctx, selected, hover);
ctx.lineWidth = this.getLineWidth(selected, hover);
let via = undefined;
if (this.options.dashes !== false) {
via = this._drawDashedLine(ctx);
this._drawDashedLine(ctx, viaNode);
}
else {
via = this._drawLine(ctx);
this._drawLine(ctx, viaNode);
}
return via;
}
_drawLine(ctx) {
let via = undefined;
_drawLine(ctx, viaNode, fromPoint, toPoint) {
if (this.from != this.to) {
// draw line
via = this._line(ctx);
this._line(ctx, viaNode, fromPoint, toPoint);
}
else {
let [x,y,radius] = this._getCircleData(ctx);
this._circle(ctx, x, y, radius);
}
return via;
}
_drawDashedLine(ctx) {
let via = undefined;
_drawDashedLine(ctx, viaNode, fromPoint, toPoint) {
ctx.lineCap = 'round';
let pattern = [5,5];
if (Array.isArray(this.options.dashes) === true) {
@ -78,7 +77,7 @@ class EdgeBase {
// draw the line
if (this.from != this.to) {
// draw line
via = this._line(ctx);
this._line(ctx, viaNode);
}
else {
let [x,y,radius] = this._getCircleData(ctx);
@ -107,7 +106,6 @@ class EdgeBase {
// disable shadows for other elements.
this.disableShadow(ctx);
}
return via;
}
@ -399,26 +397,22 @@ class EdgeBase {
return Math.sqrt(dx * dx + dy * dy);
}
/**
*
* @param ctx
* @param position
* @param viaNode
*/
drawArrowHead(ctx, position, viaNode, selected, hover) {
// set style
ctx.strokeStyle = this.getColor(ctx, selected, hover);
ctx.fillStyle = ctx.strokeStyle;
ctx.lineWidth = this.getLineWidth(selected, hover);
getArrowData(ctx, position, viaNode, selected, hover) {
// set lets
let angle;
let length;
let arrowPos;
let arrowPoint;
let node1;
let node2;
let guideOffset;
let scaleFactor;
let lineWidth = this.getLineWidth(selected, hover);
if (position === 'from') {
node1 = this.from;
@ -443,67 +437,74 @@ class EdgeBase {
if (position !== 'middle') {
// draw arrow head
if (this.options.smooth.enabled === true) {
arrowPos = this.findBorderPosition(node1, ctx, {via: viaNode});
let guidePos = this.getPoint(Math.max(0.0, Math.min(1.0, arrowPos.t + guideOffset)), viaNode);
angle = Math.atan2((arrowPos.y - guidePos.y), (arrowPos.x - guidePos.x));
arrowPoint = this.findBorderPosition(node1, ctx, {via: viaNode});
let guidePos = this.getPoint(Math.max(0.0, Math.min(1.0, arrowPoint.t + guideOffset)), viaNode);
angle = Math.atan2((arrowPoint.y - guidePos.y), (arrowPoint.x - guidePos.x));
}
else {
angle = Math.atan2((node1.y - node2.y), (node1.x - node2.x));
arrowPos = this.findBorderPosition(node1, ctx);
arrowPoint = this.findBorderPosition(node1, ctx);
}
}
else {
angle = Math.atan2((node1.y - node2.y), (node1.x - node2.x));
arrowPos = this.getPoint(0.6, viaNode); // this is 0.6 to account for the size of the arrow.
arrowPoint = this.getPoint(0.5, viaNode); // this is 0.6 to account for the size of the arrow.
}
// draw arrow at the end of the line
length = (10 + 5 * this.options.width) * scaleFactor;
ctx.arrow(arrowPos.x, arrowPos.y, angle, length);
// draw shadow if enabled
this.enableShadow(ctx);
ctx.fill();
// disable shadows for other elements.
this.disableShadow(ctx);
ctx.stroke();
}
else {
// draw circle
let angle, point;
let [x,y,radius] = this._getCircleData(ctx);
if (position === 'from') {
point = this.findBorderPosition(this.from, ctx, {x, y, low:0.25, high:0.6, direction:-1});
angle = point.t * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI;
arrowPoint = this.findBorderPosition(this.from, ctx, {x, y, low:0.25, high:0.6, direction:-1});
angle = arrowPoint.t * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI;
}
else if (position === 'to') {
point = this.findBorderPosition(this.from, ctx, {x, y, low:0.6, high:1.0, direction:1});
angle = point.t * -2 * Math.PI + 1.5 * Math.PI - 1.1 * Math.PI;
arrowPoint = this.findBorderPosition(this.from, ctx, {x, y, low:0.6, high:1.0, direction:1});
angle = arrowPoint.t * -2 * Math.PI + 1.5 * Math.PI - 1.1 * Math.PI;
}
else {
point = this._pointOnCircle(x, y, radius, 0.175);
arrowPoint = this._pointOnCircle(x, y, radius, 0.175);
angle = 3.9269908169872414; // === 0.175 * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI;
}
}
// draw the arrowhead
let length = (10 + 5 * this.options.width) * scaleFactor;
ctx.arrow(point.x, point.y, angle, length);
let length = 15 * scaleFactor + 3 * lineWidth; // 3* lineWidth is the width of the edge.
// draw shadow if enabled
this.enableShadow(ctx);
ctx.fill();
var xi = arrowPoint.x - length * 0.9 * Math.cos(angle);
var yi = arrowPoint.y - length * 0.9 * Math.sin(angle);
let arrowCore = {x: xi, y: yi};
// disable shadows for other elements.
this.disableShadow(ctx);
ctx.stroke();
}
return {point: arrowPoint, core: arrowCore, angle: angle, length: length};
}
/**
*
* @param ctx
* @param selected
* @param hover
* @param arrowData
*/
drawArrowHead(ctx, selected, hover, arrowData) {
// set style
ctx.strokeStyle = this.getColor(ctx, selected, hover);
ctx.fillStyle = ctx.strokeStyle;
ctx.lineWidth = this.getLineWidth(selected, hover);
// draw arrow at the end of the line
ctx.arrow(arrowData.point.x, arrowData.point.y, arrowData.angle, arrowData.length);
// draw shadow if enabled
this.enableShadow(ctx);
ctx.fill();
// disable shadows for other elements.
this.disableShadow(ctx);
}
enableShadow(ctx) {
if (this.options.shadow.enabled === true) {
ctx.shadowColor = 'rgba(0,0,0,0.5)';
ctx.shadowColor = this.options.shadow.color;
ctx.shadowBlur = this.options.shadow.size;
ctx.shadowOffsetX = this.options.shadow.x;
ctx.shadowOffsetY = this.options.shadow.y;

+ 21
- 16
lib/network/modules/components/nodes/shapes/Box.js View File

@ -44,34 +44,39 @@ class Box extends NodeBase {
//draw dashed border if enabled, save and restore is required for firefox not to crash on unix.
ctx.save();
this.enableBorderDashes(ctx);
//draw the border
ctx.stroke();
//disable dashed border for other elements
this.disableBorderDashes(ctx);
// if borders are zero width, they will be drawn with width 1 by default. This prevents that
if (borderWidth > 0) {
this.enableBorderDashes(ctx);
//draw the border
ctx.stroke();
//disable dashed border for other elements
this.disableBorderDashes(ctx);
}
ctx.restore();
this.updateBoundingBox(x,y);
this.updateBoundingBox(x,y,ctx,selected);
this.labelModule.draw(ctx, x, y, selected);
}
updateBoundingBox(x,y) {
updateBoundingBox(x,y, ctx, selected) {
this.resize(ctx, selected);
this.left = x - this.width * 0.5;
this.top = y - this.height * 0.5;
this.boundingBox.left = this.left;
this.boundingBox.top = this.top;
this.boundingBox.bottom = this.top + this.height;
this.boundingBox.right = this.left + this.width;
let borderRadius = this.options.shapeProperties.borderRadius; // only effective for box
this.boundingBox.left = this.left - borderRadius;
this.boundingBox.top = this.top - borderRadius;
this.boundingBox.bottom = this.top + this.height + borderRadius;
this.boundingBox.right = this.left + this.width + borderRadius;
}
distanceToBorder(ctx, angle) {
this.resize(ctx);
let a = this.width / 2;
let b = this.height / 2;
let w = (Math.sin(angle) * a);
let h = (Math.cos(angle) * b);
return a * b / Math.sqrt(w * w + h * h);
let borderWidth = this.options.borderWidth;
return Math.min(
Math.abs((this.width) / 2 / Math.cos(angle)),
Math.abs((this.height) / 2 / Math.sin(angle))) + borderWidth;
}
}

+ 1
- 5
lib/network/modules/components/nodes/shapes/Circle.js View File

@ -45,11 +45,7 @@ class Circle extends CircleImageBase {
distanceToBorder(ctx, angle) {
this.resize(ctx);
var a = this.width / 2;
var b = this.height / 2;
var w = (Math.sin(angle) * a);
var h = (Math.cos(angle) * b);
return a * b / Math.sqrt(w * w + h * h);
return this.width * 0.5;
}
}

+ 3
- 3
lib/network/modules/components/nodes/shapes/CircularImage.js View File

@ -38,10 +38,10 @@ class CircularImage extends CircleImageBase {
let size = Math.min(0.5*this.height, 0.5*this.width);
// draw the backgroun circle. IMPORTANT: the stroke in this method is used by the clip method below.
// draw the background circle. IMPORTANT: the stroke in this method is used by the clip method below.
this._drawRawCircle(ctx, x, y, selected, hover, size);
// now we draw in the cicle, we save so we can revert the clip operation after drawing.
// now we draw in the circle, we save so we can revert the clip operation after drawing.
ctx.save();
// clip is used to use the stroke in drawRawCircle as an area that we can draw in.
ctx.clip();
@ -68,7 +68,7 @@ class CircularImage extends CircleImageBase {
distanceToBorder(ctx, angle) {
this.resize(ctx);
return this._distanceToBorder(angle);
return this.width * 0.5;
}
}

+ 12
- 15
lib/network/modules/components/nodes/shapes/Database.js View File

@ -23,13 +23,12 @@ class Database extends NodeBase {
this.left = x - this.width / 2;
this.top = y - this.height / 2;
var borderWidth = this.options.borderWidth;
var neutralborderWidth = this.options.borderWidth;
var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
var borderWidth = (selected ? selectionLineWidth : neutralborderWidth) / this.body.view.scale;
ctx.lineWidth = Math.min(this.width, borderWidth);
ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border;
ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth);
ctx.lineWidth *= this.networkScaleInv;
ctx.lineWidth = Math.min(this.width, ctx.lineWidth);
ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background;
ctx.database(x - this.width / 2, y - this.height * 0.5, this.width, this.height);
@ -43,11 +42,14 @@ class Database extends NodeBase {
//draw dashed border if enabled, save and restore is required for firefox not to crash on unix.
ctx.save();
this.enableBorderDashes(ctx);
//draw the border
ctx.stroke();
//disable dashed border for other elements
this.disableBorderDashes(ctx);
// if borders are zero width, they will be drawn with width 1 by default. This prevents that
if (borderWidth > 0) {
this.enableBorderDashes(ctx);
//draw the border
ctx.stroke();
//disable dashed border for other elements
this.disableBorderDashes(ctx);
}
ctx.restore();
this.updateBoundingBox(x,y,ctx,selected);
@ -67,12 +69,7 @@ class Database extends NodeBase {
}
distanceToBorder(ctx, angle) {
this.resize(ctx);
var a = this.width / 2;
var b = this.height / 2;
var w = (Math.sin(angle) * a);
var h = (Math.cos(angle) * b);
return a * b / Math.sqrt(w * w + h * h);
return this._distanceToBorder(ctx,angle);
}
}

+ 1
- 1
lib/network/modules/components/nodes/shapes/Diamond.js View File

@ -16,7 +16,7 @@ class Diamond extends ShapeBase {
}
distanceToBorder(ctx, angle) {
return this._distanceToBorder(angle);
return this._distanceToBorder(ctx,angle);
}
}

+ 2
- 1
lib/network/modules/components/nodes/shapes/Dot.js View File

@ -16,7 +16,8 @@ class Dot extends ShapeBase {
}
distanceToBorder(ctx, angle) {
return this.options.size + this.options.borderWidth;
this.resize(ctx);
return this.options.size;
}
}

+ 13
- 10
lib/network/modules/components/nodes/shapes/Ellipse.js View File

@ -25,15 +25,13 @@ class Ellipse extends NodeBase {
this.left = x - this.width * 0.5;
this.top = y - this.height * 0.5;
var borderWidth = this.options.borderWidth;
var neutralborderWidth = this.options.borderWidth;
var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
var borderWidth = (selected ? selectionLineWidth : neutralborderWidth) / this.body.view.scale;
ctx.lineWidth = Math.min(this.width, borderWidth);
ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border;
ctx.lineWidth = (selected ? selectionLineWidth : borderWidth);
ctx.lineWidth /= this.body.view.scale;
ctx.lineWidth = Math.min(this.width, ctx.lineWidth);
ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background;
ctx.ellipse(this.left, this.top, this.width, this.height);
@ -46,11 +44,16 @@ class Ellipse extends NodeBase {
//draw dashed border if enabled, save and restore is required for firefox not to crash on unix.
ctx.save();
this.enableBorderDashes(ctx);
//draw the border
ctx.stroke();
//disable dashed border for other elements
this.disableBorderDashes(ctx);
// if borders are zero width, they will be drawn with width 1 by default. This prevents that
if (borderWidth > 0) {
this.enableBorderDashes(ctx);
//draw the border
ctx.stroke();
//disable dashed border for other elements
this.disableBorderDashes(ctx);
}
ctx.restore();
this.updateBoundingBox(x, y, ctx, selected);

+ 1
- 2
lib/network/modules/components/nodes/shapes/Icon.js View File

@ -75,8 +75,7 @@ class Icon extends NodeBase {
}
distanceToBorder(ctx, angle) {
this.resize(ctx);
return this._distanceToBorder(angle);
return this._distanceToBorder(ctx,angle);
}
}

+ 37
- 6
lib/network/modules/components/nodes/shapes/Image.js View File

@ -17,6 +17,42 @@ class Image extends CircleImageBase {
this.left = x - this.width / 2;
this.top = y - this.height / 2;
if (this.options.shapeProperties.useBorderWithImage === true) {
var neutralborderWidth = this.options.borderWidth;
var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
var borderWidth = (selected ? selectionLineWidth : neutralborderWidth) / this.body.view.scale;
ctx.lineWidth = Math.min(this.width, borderWidth);
ctx.beginPath();
// setup the line properties.
ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border;
// set a fillstyle
ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background;
// draw a rectangle to form the border around. This rectangle is filled so the opacity of a picture (in future vis releases?) can be used to tint the image
ctx.rect(this.left - 0.5 * ctx.lineWidth,
this.top - 0.5 * ctx.lineWidth,
this.width + ctx.lineWidth,
this.height + ctx.lineWidth);
ctx.fill();
//draw dashed border if enabled, save and restore is required for firefox not to crash on unix.
ctx.save();
// if borders are zero width, they will be drawn with width 1 by default. This prevents that
if (borderWidth > 0) {
this.enableBorderDashes(ctx);
//draw the border
ctx.stroke();
//disable dashed border for other elements
this.disableBorderDashes(ctx);
}
ctx.restore();
ctx.closePath();
}
this._drawImageAtPosition(ctx);
this._drawImageLabel(ctx, x, y, selected || hover);
@ -42,12 +78,7 @@ class Image extends CircleImageBase {
}
distanceToBorder(ctx, angle) {
this.resize(ctx);
var a = this.width / 2;
var b = this.height / 2;
var w = (Math.sin(angle) * a);
var h = (Math.cos(angle) * b);
return a * b / Math.sqrt(w * w + h * h);
return this._distanceToBorder(ctx,angle);
}
}

+ 1
- 2
lib/network/modules/components/nodes/shapes/Square.js View File

@ -16,8 +16,7 @@ class Square extends ShapeBase {
}
distanceToBorder(ctx, angle) {
this.resize();
return this._distanceToBorder(angle);
return this._distanceToBorder(ctx,angle);
}
}

+ 1
- 1
lib/network/modules/components/nodes/shapes/Star.js View File

@ -16,7 +16,7 @@ class Star extends ShapeBase {
}
distanceToBorder(ctx, angle) {
return this._distanceToBorder(angle);
return this._distanceToBorder(ctx,angle);
}
}

+ 1
- 2
lib/network/modules/components/nodes/shapes/Text.js View File

@ -45,8 +45,7 @@ class Text extends NodeBase {
}
distanceToBorder(ctx, angle) {
this.resize(ctx);
return this._distanceToBorder(angle);
return this._distanceToBorder(ctx,angle);
}
}

+ 1
- 1
lib/network/modules/components/nodes/shapes/Triangle.js View File

@ -16,7 +16,7 @@ class Triangle extends ShapeBase {
}
distanceToBorder(ctx, angle) {
return this._distanceToBorder(angle);
return this._distanceToBorder(ctx,angle);
}
}

+ 1
- 1
lib/network/modules/components/nodes/shapes/TriangleDown.js View File

@ -16,7 +16,7 @@ class TriangleDown extends ShapeBase {
}
distanceToBorder(ctx, angle) {
return this._distanceToBorder(angle);
return this._distanceToBorder(ctx,angle);
}
}

+ 42
- 12
lib/network/modules/components/nodes/util/CircleImageBase.js View File

@ -66,14 +66,12 @@ class CircleImageBase extends NodeBase {
}
_drawRawCircle(ctx, x, y, selected, hover, size) {
var borderWidth = this.options.borderWidth;
var neutralborderWidth = this.options.borderWidth;
var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
var borderWidth = (selected ? selectionLineWidth : neutralborderWidth) / this.body.view.scale;
ctx.lineWidth = Math.min(this.width, borderWidth);
ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border;
ctx.lineWidth = (selected ? selectionLineWidth : borderWidth);
ctx.lineWidth *= this.networkScaleInv;
ctx.lineWidth = Math.min(this.width, ctx.lineWidth);
ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background;
ctx.circle(x, y, size);
@ -86,11 +84,14 @@ class CircleImageBase extends NodeBase {
//draw dashed border if enabled, save and restore is required for firefox not to crash on unix.
ctx.save();
this.enableBorderDashes(ctx);
//draw the border
ctx.stroke();
//disable dashed border for other elements
this.disableBorderDashes(ctx);
// if borders are zero width, they will be drawn with width 1 by default. This prevents that
if (borderWidth > 0) {
this.enableBorderDashes(ctx);
//draw the border
ctx.stroke();
//disable dashed border for other elements
this.disableBorderDashes(ctx);
}
ctx.restore();
}
@ -102,8 +103,37 @@ class CircleImageBase extends NodeBase {
// draw shadow if enabled
this.enableShadow(ctx);
// draw image
ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
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);
}
// disable shadows for other elements.
this.disableShadow(ctx);

+ 4
- 3
lib/network/modules/components/nodes/util/NodeBase.js View File

@ -15,8 +15,9 @@ class NodeBase {
this.options = options;
}
_distanceToBorder(angle) {
var borderWidth = 1;
_distanceToBorder(ctx,angle) {
var borderWidth = this.options.borderWidth;
this.resize(ctx);
return Math.min(
Math.abs(this.width / 2 / Math.cos(angle)),
Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
@ -24,7 +25,7 @@ class NodeBase {
enableShadow(ctx) {
if (this.options.shadow.enabled === true) {
ctx.shadowColor = 'rgba(0,0,0,0.5)';
ctx.shadowColor = this.options.shadow.color;
ctx.shadowBlur = this.options.shadow.size;
ctx.shadowOffsetX = this.options.shadow.x;
ctx.shadowOffsetY = this.options.shadow.y;

+ 11
- 9
lib/network/modules/components/nodes/util/ShapeBase.js View File

@ -20,13 +20,12 @@ class ShapeBase extends NodeBase {
this.left = x - this.width / 2;
this.top = y - this.height / 2;
var borderWidth = this.options.borderWidth;
var neutralborderWidth = this.options.borderWidth;
var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
var borderWidth = (selected ? selectionLineWidth : neutralborderWidth) / this.body.view.scale;
ctx.lineWidth = Math.min(this.width, borderWidth);
ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border;
ctx.lineWidth = (selected ? selectionLineWidth : borderWidth);
ctx.lineWidth /= this.body.view.scale;
ctx.lineWidth = Math.min(this.width, ctx.lineWidth);
ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background;
ctx[shape](x, y, this.options.size);
@ -39,11 +38,14 @@ class ShapeBase extends NodeBase {
//draw dashed border if enabled, save and restore is required for firefox not to crash on unix.
ctx.save();
this.enableBorderDashes(ctx);
//draw the border
ctx.stroke();
//disable dashed border for other elements
this.disableBorderDashes(ctx);
// if borders are zero width, they will be drawn with width 1 by default. This prevents that
if (borderWidth > 0) {
this.enableBorderDashes(ctx);
//draw the border
ctx.stroke();
//disable dashed border for other elements
this.disableBorderDashes(ctx);
}
ctx.restore();
if (this.options.label !== undefined) {

+ 5
- 2
lib/network/modules/components/physics/BarnesHutSolver.js View File

@ -6,6 +6,9 @@ class BarnesHutSolver {
this.barnesHutTree;
this.setOptions(options);
this.randomSeed = 5;
// debug: show grid
//this.body.emitter.on("afterDrawing", (ctx) => {this._debug(ctx,'#ff0000')})
}
setOptions(options) {
@ -21,7 +24,7 @@ class BarnesHutSolver {
/**
* This function calculates the forces the nodes apply on eachother based on a gravitational model.
* This function calculates the forces the nodes apply on each other based on a gravitational model.
* The Barnes Hut method is used to speed up this N-body simulation.
*
* @private
@ -285,7 +288,7 @@ class BarnesHutSolver {
break;
case 1: // convert into children
// if there are two nodes exactly overlapping (on init, on opening of cluster etc.)
// we move one node a pixel and we do not put it in the tree.
// we move one node a little bit and we do not put it in the tree.
if (parentBranch.children[region].children.data.x === node.x &&
parentBranch.children[region].children.data.y === node.y) {
node.x += this.seededRandom();

+ 19
- 0
lib/network/options.js View File

@ -29,6 +29,7 @@ let allOptions = {
from: { enabled: { boolean }, scaleFactor: { number }, __type__: { object, boolean } },
__type__: { string: ['from', 'to', 'middle'], object }
},
arrowStrikethrough: { boolean },
color: {
color: { string },
highlight: { string },
@ -72,6 +73,7 @@ let allOptions = {
selfReferenceSize: { number },
shadow: {
enabled: { boolean },
color: { string },
size: { number },
x: { number },
y: { number },
@ -121,6 +123,11 @@ let allOptions = {
hierarchical: {
enabled: { boolean },
levelSeparation: { number },
nodeSpacing: { number },
treeSpacing: { number },
blockShifting: { boolean },
edgeMinimization: { boolean },
parentCentralization: { boolean },
direction: { string: ['UD', 'DU', 'LR', 'RL'] }, // UD, DU, LR, RL
sortMethod: { string: ['hubsize', 'directed'] }, // hubsize, directed
__type__: { object, boolean }
@ -204,6 +211,7 @@ let allOptions = {
},
shadow: {
enabled: { boolean },
color: { string },
size: { number },
x: { number },
y: { number },
@ -213,7 +221,9 @@ let allOptions = {
shapeProperties: {
borderDashes: { boolean, array },
borderRadius: { number },
interpolation: { boolean },
useImageSize: { boolean },
useBorderWithImage: { boolean },
__type__: { object }
},
size: { number },
@ -344,6 +354,7 @@ let configureOptions = {
},
shadow: {
enabled: false,
color: 'rgba(0,0,0,0.5)',
size: [10, 0, 20, 1],
x: [5, -30, 30, 1],
y: [5, -30, 30, 1]
@ -352,6 +363,7 @@ let configureOptions = {
shapeProperties: {
borderDashes: false,
borderRadius: [6, 0, 20, 1],
interpolation: true,
useImageSize: false
},
size: [25, 0, 200, 1]
@ -362,6 +374,7 @@ let configureOptions = {
middle: { enabled: false, scaleFactor: [1, 0, 3, 0.05] },
from: { enabled: false, scaleFactor: [1, 0, 3, 0.05] }
},
arrowStrikethrough: true,
color: {
color: ['color', '#848484'],
highlight: ['color', '#848484'],
@ -398,6 +411,7 @@ let configureOptions = {
selfReferenceSize: [20, 0, 200, 1],
shadow: {
enabled: false,
color: 'rgba(0,0,0,0.5)',
size: [10, 0, 20, 1],
x: [5, -30, 30, 1],
y: [5, -30, 30, 1]
@ -416,6 +430,11 @@ let configureOptions = {
hierarchical: {
enabled: false,
levelSeparation: [150, 20, 500, 5],
nodeSpacing: [100, 20, 500, 5],
treeSpacing: [200, 20, 500, 5],
blockShifting: true,
edgeMinimization: true,
parentCentralization: true,
direction: ['UD', 'DU', 'LR', 'RL'], // UD, DU, LR, RL
sortMethod: ['hubsize', 'directed'] // hubsize, directed
}

+ 34
- 10
lib/shared/ColorPicker.js View File

@ -16,6 +16,7 @@ class ColorPicker {
// bound by
this.updateCallback = () => {};
this.closeCallback = () => {};
// create all DOM elements
this._create();
@ -42,12 +43,25 @@ class ColorPicker {
* the callback is executed on apply and save. Bind it to the application
* @param callback
*/
setCallback(callback) {
setUpdateCallback(callback) {
if (typeof callback === 'function') {
this.updateCallback = callback;
}
else {
throw new Error("Function attempted to set as colorPicker callback is not a function.");
throw new Error("Function attempted to set as colorPicker update callback is not a function.");
}
}
/**
* the callback is executed on apply and save. Bind it to the application
* @param callback
*/
setCloseCallback(callback) {
if (typeof callback === 'function') {
this.closeCallback = callback;
}
else {
throw new Error("Function attempted to set as colorPicker closing callback is not a function.");
}
}
@ -119,19 +133,20 @@ class ColorPicker {
/**
* this shows the color picker at a location. The hue circle is constructed once and stored.
* @param x
* @param y
* this shows the color picker.
* The hue circle is constructed once and stored.
*/
show(x,y) {
show() {
if (this.closeCallback !== undefined) {
this.closeCallback();
this.closeCallback = undefined;
}
this.applied = false;
this.frame.style.display = 'block';
this.frame.style.top = y + 'px';
this.frame.style.left = x + 'px';
this._generateHueCircle();
}
// ------------------------------------------ PRIVATE ----------------------------- //
/**
@ -151,6 +166,15 @@ class ColorPicker {
}
this.frame.style.display = 'none';
// call the closing callback, restoring the onclick method.
// this is in a setTimeout because it will trigger the show again before the click is done.
setTimeout(() => {
if (this.closeCallback !== undefined) {
this.closeCallback();
this.closeCallback = undefined;
}
},0);
}
@ -244,7 +268,7 @@ class ColorPicker {
/**
* update the colorpicker. A black circle overlays the hue circle to mimic the brightness decreasing.
* update the color picker. A black circle overlays the hue circle to mimic the brightness decreasing.
* @param rgba
* @private
*/

+ 17
- 10
lib/shared/Configurator.js View File

@ -140,7 +140,7 @@ class Configurator {
// a header for the category
this._makeHeader(option);
// get the suboptions
// get the sub options
this._handleObject(this.configureOptions[option], [option]);
}
counter++;
@ -163,7 +163,7 @@ class Configurator {
}
this._push();
this.colorPicker.insertTo(this.container);
//~ this.colorPicker.insertTo(this.container);
}
@ -530,17 +530,25 @@ class Configurator {
* @private
*/
_showColorPicker(value, div, path) {
let rect = div.getBoundingClientRect();
let bodyRect = document.body.getBoundingClientRect();
let pickerX = rect.left + rect.width + 5;
let pickerY = rect.top - bodyRect.top + rect.height +2;
this.colorPicker.show(pickerX,pickerY);
// clear the callback from this div
div.onclick = function() {};
this.colorPicker.insertTo(div);
this.colorPicker.show();
this.colorPicker.setColor(value);
this.colorPicker.setCallback((color) => {
this.colorPicker.setUpdateCallback((color) => {
let colorString = 'rgba(' + color.r + ',' + color.g + ',' + color.b + ',' + color.a + ')';
div.style.backgroundColor = colorString;
this._update(colorString,path);
})
});
// on close of the colorpicker, restore the callback.
this.colorPicker.setCloseCallback(() => {
div.onclick = () => {
this._showColorPicker(value,div,path);
};
});
}
@ -687,7 +695,6 @@ class Configurator {
}
}
return optionsObj;
}
_printOptions() {

+ 6
- 1
lib/shared/configuration.css View File

@ -10,6 +10,11 @@ div.vis-configuration-wrapper {
width:700px;
}
div.vis-configuration-wrapper::after {
clear: both;
content: "";
display: block;
}
div.vis-configuration.vis-config-option-container{
display:block;
@ -111,7 +116,7 @@ input.vis-configuration.vis-config-rangeinput{
position:relative;
top:-5px;
width:60px;
height:13px;
/*height:13px;*/
padding:1px;
margin:0;
pointer-events:none;

+ 55
- 15
lib/timeline/Core.js View File

@ -91,13 +91,15 @@ Core.prototype._create = function (container) {
this.dom.rightContainer.appendChild(this.dom.shadowBottomRight);
this.on('rangechange', function () {
this._redraw(); // this allows overriding the _redraw method
if (this.initialDrawDone === true) {
this._redraw(); // this allows overriding the _redraw method
}
}.bind(this));
this.on('touch', this._onTouch.bind(this));
this.on('pan', this._onDrag.bind(this));
var me = this;
this.on('change', function (properties) {
this.on('_change', function (properties) {
if (properties && properties.queue == true) {
// redraw once on next tick
if (!me._redrawTimer) {
@ -116,8 +118,9 @@ Core.prototype._create = function (container) {
// create event listeners for all interesting events, these events will be
// emitted via emitter
this.hammer = new Hammer(this.dom.root);
this.hammer.get('pinch').set({enable: true});
this.hammer.get('pan').set({threshold:5, direction:30}); // 30 is ALL_DIRECTIONS in hammer.
var pinchRecognizer = this.hammer.get('pinch').set({enable: true});
hammerUtil.disablePreventDefaultVertically(pinchRecognizer);
this.hammer.get('pan').set({threshold:5, direction: Hammer.DIRECTION_HORIZONTAL});
this.listeners = {};
var events = [
@ -181,6 +184,7 @@ Core.prototype._create = function (container) {
this.touch = {};
this.redrawCount = 0;
this.initialDrawDone = false;
// attach the root panel to the provided container
if (!container) throw new Error('No container provided');
@ -223,6 +227,7 @@ Core.prototype.setOptions = function (options) {
];
util.selectiveExtend(fields, this.options, options);
this.options.orientation = {item:undefined,axis:undefined};
if ('orientation' in options) {
if (typeof options.orientation === 'string') {
this.options.orientation = {
@ -317,11 +322,12 @@ Core.prototype.setOptions = function (options) {
// override redraw with a throttled version
if (!this._origRedraw) {
this._origRedraw = this._redraw.bind(this);
this._redraw = util.throttle(this._origRedraw, this.options.throttleRedraw);
} else {
// Not the initial run: redraw everything
this._redraw();
}
this._redraw = util.throttle(this._origRedraw, this.options.throttleRedraw);
// redraw everything
this._redraw();
};
/**
@ -409,6 +415,24 @@ Core.prototype.getCustomTime = function(id) {
return customTimes[0].getCustomTime();
};
/**
* Set a custom title for the custom time bar.
* @param {String} [title] Custom title
* @param {number} [id=undefined] Id of the custom time bar.
*/
Core.prototype.setCustomTimeTitle = function(title, id) {
var customTimes = this.customTimes.filter(function (component) {
return component.options.id === id;
});
if (customTimes.length === 0) {
throw new Error('No custom time bar found with id ' + JSON.stringify(id))
}
if (customTimes.length > 0) {
return customTimes[0].setCustomTitle(title);
}
};
/**
* Retrieve meta information from an event.
* Should be overridden by classes extending Core
@ -603,12 +627,13 @@ Core.prototype.redraw = function() {
* @protected
*/
Core.prototype._redraw = function() {
this.redrawCount++;
var resized = false;
var options = this.options;
var props = this.props;
var dom = this.dom;
if (!dom) return; // when destroyed
if (!dom|| !dom.container || dom.container.clientWidth == 0 ) return;// when destroyed, or invisible
DateUtil.updateHiddenDates(this.options.moment, this.body, this.options.hiddenDates);
@ -744,22 +769,32 @@ Core.prototype._redraw = function() {
dom.shadowTopRight.style.visibility = visibilityTop;
dom.shadowBottomRight.style.visibility = visibilityBottom;
// enable/disable vertical panning
var contentsOverflow = this.props.center.height > this.props.centerContainer.height;
this.hammer.get('pan').set({
direction: contentsOverflow ? Hammer.DIRECTION_ALL : Hammer.DIRECTION_HORIZONTAL
});
// redraw all components
this.components.forEach(function (component) {
resized = component.redraw() || resized;
});
var MAX_REDRAW = 5;
if (resized) {
// keep repainting until all sizes are settled
var MAX_REDRAWS = 3; // maximum number of consecutive redraws
if (this.redrawCount < MAX_REDRAWS) {
this.redrawCount++;
this._redraw();
if (this.redrawCount < MAX_REDRAW) {
this.body.emitter.emit('_change');
return;
}
else {
console.log('WARNING: infinite loop in redraw?');
}
} else {
this.redrawCount = 0;
}
this.initialDrawDone = true;
//Emit public 'changed' event for UI updates, see issue #1592
this.body.emitter.emit("changed");
};
// TODO: deprecated since version 1.1.0, remove some day
@ -889,7 +924,7 @@ Core.prototype._startAutoResize = function () {
me.props.lastWidth = me.dom.root.offsetWidth;
me.props.lastHeight = me.dom.root.offsetHeight;
me.emit('change');
me.body.emitter.emit('_change');
}
}
};
@ -897,6 +932,12 @@ Core.prototype._startAutoResize = function () {
// add event listener to window resize
util.addEventListener(window, 'resize', this._onResize);
//Prevent initial unnecessary redraw
if (me.dom.root) {
me.props.lastWidth = me.dom.root.offsetWidth;
me.props.lastHeight = me.dom.root.offsetHeight;
}
this.watchTimer = setInterval(this._onResize, 1000);
};
@ -953,7 +994,6 @@ Core.prototype._onDrag = function (event) {
if (newScrollTop != oldScrollTop) {
this._redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already
this.emit("verticalDrag");
}
};

+ 0
- 233
lib/timeline/DataStep.js View File

@ -1,233 +0,0 @@
/**
* @constructor DataStep
* The class DataStep is an iterator for data for the lineGraph. You provide a start data point and an
* end data point. The class itself determines the best scale (step size) based on the
* provided start Date, end Date, and minimumStep.
*
* If minimumStep is provided, the step size is chosen as close as possible
* to the minimumStep but larger than minimumStep. If minimumStep is not
* provided, the scale is set to 1 DAY.
* The minimumStep should correspond with the onscreen size of about 6 characters
*
* Alternatively, you can set a scale by hand.
* After creation, you can initialize the class by executing first(). Then you
* can iterate from the start date to the end date via next(). You can check if
* the end date is reached with the function hasNext(). After each step, you can
* retrieve the current date via getCurrent().
* The DataStep has scales ranging from milliseconds, seconds, minutes, hours,
* days, to years.
*
* Version: 1.2
*
* @param {Date} [start] The start date, for example new Date(2010, 9, 21)
* or new Date(2010, 9, 21, 23, 45, 00)
* @param {Date} [end] The end date
* @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
*/
function DataStep(start, end, minimumStep, containerHeight, customRange, formattingFunction, alignZeros) {
// variables
this.current = 0;
this.autoScale = true;
this.stepIndex = 0;
this.step = 1;
this.scale = 1;
this.formattingFunction = formattingFunction;
this.marginStart;
this.marginEnd;
this.deadSpace = 0;
this.majorSteps = [1, 2, 5, 10];
this.minorSteps = [0.25, 0.5, 1, 2];
this.alignZeros = alignZeros;
this.setRange(start, end, minimumStep, containerHeight, customRange);
}
/**
* Set a new range
* If minimumStep is provided, the step size is chosen as close as possible
* to the minimumStep but larger than minimumStep. If minimumStep is not
* provided, the scale is set to 1 DAY.
* The minimumStep should correspond with the onscreen size of about 6 characters
* @param {Number} [start] The start date and time.
* @param {Number} [end] The end date and time.
* @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
*/
DataStep.prototype.setRange = function(start, end, minimumStep, containerHeight, customRange) {
this._start = customRange.min === undefined ? start : customRange.min;
this._end = customRange.max === undefined ? end : customRange.max;
if (this._start === this._end) {
this._start = customRange.min === undefined ? this._start - 0.75 : this._start;
this._end = customRange.max === undefined ? this._end + 1 : this._end;;
}
if (this.autoScale === true) {
this.setMinimumStep(minimumStep, containerHeight);
}
this.setFirst(customRange);
};
/**
* Automatically determine the scale that bests fits the provided minimum step
* @param {Number} [minimumStep] The minimum step size in pixels
*/
DataStep.prototype.setMinimumStep = function(minimumStep, containerHeight) {
// round to floor
var range = this._end - this._start;
var safeRange = range * 1.2;
var minimumStepValue = minimumStep * (safeRange / containerHeight);
var orderOfMagnitude = Math.round(Math.log(safeRange)/Math.LN10);
var minorStepIdx = -1;
var magnitudefactor = Math.pow(10,orderOfMagnitude);
var start = 0;
if (orderOfMagnitude < 0) {
start = orderOfMagnitude;
}
var solutionFound = false;
for (var i = start; Math.abs(i) <= Math.abs(orderOfMagnitude); i++) {
magnitudefactor = Math.pow(10,i);
for (var j = 0; j < this.minorSteps.length; j++) {
var stepSize = magnitudefactor * this.minorSteps[j];
if (stepSize >= minimumStepValue) {
solutionFound = true;
minorStepIdx = j;
break;
}
}
if (solutionFound === true) {
break;
}
}
this.stepIndex = minorStepIdx;
this.scale = magnitudefactor;
this.step = magnitudefactor * this.minorSteps[minorStepIdx];
};
/**
* Round the current date to the first minor date value
* This must be executed once when the current date is set to start Date
*/
DataStep.prototype.setFirst = function(customRange) {
if (customRange === undefined) {
customRange = {};
}
var niceStart = customRange.min === undefined ? this._start - (this.scale * 2 * this.minorSteps[this.stepIndex]) : customRange.min;
var niceEnd = customRange.max === undefined ? this._end + (this.scale * this.minorSteps[this.stepIndex]) : customRange.max;
this.marginEnd = customRange.max === undefined ? this.roundToMinor(niceEnd) : customRange.max;
this.marginStart = customRange.min === undefined ? this.roundToMinor(niceStart) : customRange.min;
// if we need to align the zero's we need to make sure that there is a zero to use.
if (this.alignZeros === true && (this.marginEnd - this.marginStart) % this.step != 0) {
this.marginEnd += this.marginEnd % this.step;
}
this.deadSpace = this.roundToMinor(niceEnd) - niceEnd + this.roundToMinor(niceStart) - niceStart;
this.marginRange = this.marginEnd - this.marginStart;
this.current = this.marginEnd;
};
DataStep.prototype.roundToMinor = function(value) {
var rounded = value - (value % (this.scale * this.minorSteps[this.stepIndex]));
if (value % (this.scale * this.minorSteps[this.stepIndex]) > 0.5 * (this.scale * this.minorSteps[this.stepIndex])) {
return rounded + (this.scale * this.minorSteps[this.stepIndex]);
}
else {
return rounded;
}
}
/**
* Check if the there is a next step
* @return {boolean} true if the current date has not passed the end date
*/
DataStep.prototype.hasNext = function () {
return (this.current >= this.marginStart);
};
/**
* Do the next step
*/
DataStep.prototype.next = function() {
var prev = this.current;
this.current -= this.step;
// safety mechanism: if current time is still unchanged, move to the end
if (this.current === prev) {
this.current = this._end;
}
};
/**
* Do the next step
*/
DataStep.prototype.previous = function() {
this.current += this.step;
this.marginEnd += this.step;
this.marginRange = this.marginEnd - this.marginStart;
};
/**
* Get the current datetime
* @return {String} current The current date
*/
DataStep.prototype.getCurrent = function() {
// prevent round-off errors when close to zero
var current = (Math.abs(this.current) < this.step / 2) ? 0 : this.current;
var returnValue = current.toPrecision(5);
if (typeof this.formattingFunction === 'function') {
returnValue = this.formattingFunction(current);
}
if (typeof returnValue === 'number') {
return '' + returnValue;
}
else if (typeof returnValue === 'string') {
return returnValue;
}
else {
return current.toPrecision(5);
}
};
/**
* Check if the current value is a major value (for example when the step
* is DAY, a major value is each first day of the MONTH)
* @return {boolean} true if current date is major, else false.
*/
DataStep.prototype.isMajor = function() {
return (this.current % (this.scale * this.majorSteps[this.stepIndex]) === 0);
};
DataStep.prototype.shift = function(steps) {
if (steps < 0) {
for (let i = 0; i < -steps; i++) {
this.previous();
}
}
else if (steps > 0) {
for (let i = 0; i < steps; i++) {
this.next();
}
}
}
module.exports = DataStep;

+ 11
- 3
lib/timeline/DateUtil.js View File

@ -4,9 +4,13 @@
*
* @param {function} moment
* @param {Object} body
* @param {Array} hiddenDates
* @param {Array | Object} hiddenDates
*/
exports.convertHiddenOptions = function(moment, body, hiddenDates) {
if (hiddenDates && !Array.isArray(hiddenDates)) {
return exports.convertHiddenOptions(moment, body, [hiddenDates])
}
body.hiddenDates = [];
if (hiddenDates) {
if (Array.isArray(hiddenDates) == true) {
@ -30,9 +34,13 @@ exports.convertHiddenOptions = function(moment, body, hiddenDates) {
* create new entrees for the repeating hidden dates
* @param {function} moment
* @param {Object} body
* @param {Array} hiddenDates
* @param {Array | Object} hiddenDates
*/
exports.updateHiddenDates = function (moment, body, hiddenDates) {
if (hiddenDates && !Array.isArray(hiddenDates)) {
return exports.updateHiddenDates(moment, body, [hiddenDates])
}
if (hiddenDates && body.domProps.centerContainer.width !== undefined) {
exports.convertHiddenOptions(moment, body, hiddenDates);
@ -135,7 +143,7 @@ exports.updateHiddenDates = function (moment, body, hiddenDates) {
case "weekly":
startDate.add(1, 'weeks');
endDate.add(1, 'weeks');
break
break;
case "monthly":
startDate.add(1, 'months');
endDate.add(1, 'months');

+ 7
- 6
lib/timeline/Graph2d.js View File

@ -27,7 +27,7 @@ var configureOptions = require('./optionsGraph2d').configureOptions;
*/
function Graph2d (container, items, groups, options) {
// if the third element is options, the forth is groups (optionally);
if (!(Array.isArray(groups) || groups instanceof DataSet) && groups instanceof Object) {
if (!(Array.isArray(groups) || groups instanceof DataSet || groups instanceof DataView) && groups instanceof Object) {
var forthArgument = options;
options = groups;
groups = forthArgument;
@ -93,11 +93,13 @@ function Graph2d (container, items, groups, options) {
// item set
this.linegraph = new LineGraph(this.body);
this.components.push(this.linegraph);
this.itemsData = null; // DataSet
this.groupsData = null; // DataSet
this.on('tap', function (event) {
me.emit('click', me.getEventProperties(event))
});
@ -122,9 +124,9 @@ function Graph2d (container, items, groups, options) {
if (items) {
this.setItems(items);
}
else {
this._redraw();
}
// draw for the first time
this._redraw();
}
// Extend the functionality from Core
@ -173,7 +175,6 @@ Graph2d.prototype.setItems = function(items) {
if (this.options.start != undefined || this.options.end != undefined) {
var start = this.options.start != undefined ? this.options.start : null;
var end = this.options.end != undefined ? this.options.end : null;
this.setWindow(start, end, {animation: false});
}
else {
@ -217,7 +218,7 @@ Graph2d.prototype.getLegend = function(groupId, width, height) {
return this.linegraph.groups[groupId].getLegend(width,height);
}
else {
return "cannot find group:" + groupId;
return "cannot find group:'" + groupId + "'";
}
};

+ 7
- 3
lib/timeline/Range.js View File

@ -429,10 +429,14 @@ Range.prototype._onDrag = function (event) {
this.previousDelta = delta;
this._applyRange(newStart, newEnd);
var startDate = new Date(this.start);
var endDate = new Date(this.end);
// fire a rangechange event
this.body.emitter.emit('rangechange', {
start: new Date(this.start),
end: new Date(this.end),
start: startDate,
end: endDate,
byUser: true
});
};
@ -551,7 +555,7 @@ Range.prototype._onPinch = function (event) {
var scale = 1 / (event.scale + this.scaleOffset);
var centerDate = this._pointerToDate(this.props.touch.center);
var hiddenDuration = DateUtil.getHiddenDurationBetween(this.options.moment, this.body.hiddenDates, this.start, this.end);
var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end);
var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.options.moment, this.body.hiddenDates, this, centerDate);
var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore;

+ 7
- 2
lib/timeline/TimeStep.js View File

@ -47,8 +47,13 @@ function TimeStep(start, end, minimumStep, hiddenDates) {
this.switchedDay = false;
this.switchedMonth = false;
this.switchedYear = false;
this.hiddenDates = hiddenDates;
if (hiddenDates === undefined) {
if (Array.isArray(hiddenDates)) {
this.hiddenDates = hiddenDates;
}
else if (hiddenDates != undefined) {
this.hiddenDates = [hiddenDates];
}
else {
this.hiddenDates = [];
}

+ 31
- 26
lib/timeline/Timeline.js View File

@ -121,6 +121,28 @@ function Timeline (container, items, groups, options) {
me.emit('contextmenu', me.getEventProperties(event))
};
//Single time autoscale/fit
this.fitDone = false;
this.on('changed', function (){
if (this.itemsData == null) return;
if (!me.fitDone) {
me.fitDone = true;
if (me.options.start != undefined || me.options.end != undefined) {
if (me.options.start == undefined || me.options.end == undefined) {
var range = me.getItemRange();
}
var start = me.options.start != undefined ? me.options.start : range.min;
var end = me.options.end != undefined ? me.options.end : range.max;
me.setWindow(start, end, {animation: false});
}
else {
me.fit({animation: false});
}
}
});
// apply options
if (options) {
this.setOptions(options);
@ -135,9 +157,9 @@ function Timeline (container, items, groups, options) {
if (items) {
this.setItems(items);
}
else {
this._redraw();
}
// draw for the first time
this._redraw();
}
// Extend the functionality from Core
@ -194,8 +216,6 @@ Timeline.prototype.setOptions = function (options) {
* @param {vis.DataSet | Array | null} items
*/
Timeline.prototype.setItems = function(items) {
var initialLoad = (this.itemsData == null);
// convert to type DataSet when needed
var newDataSet;
if (!items) {
@ -217,22 +237,6 @@ Timeline.prototype.setItems = function(items) {
// set items
this.itemsData = newDataSet;
this.itemSet && this.itemSet.setItems(newDataSet);
if (initialLoad) {
if (this.options.start != undefined || this.options.end != undefined) {
if (this.options.start == undefined || this.options.end == undefined) {
var range = this.getItemRange();
}
var start = this.options.start != undefined ? this.options.start : range.min;
var end = this.options.end != undefined ? this.options.end : range.max;
this.setWindow(start, end, {animation: false});
}
else {
this.fit({animation: false});
}
}
};
/**
@ -379,8 +383,8 @@ Timeline.prototype.fit = function (options) {
Timeline.prototype.getItemRange = function () {
// get a rough approximation for the range based on the items start and end dates
var range = this.getDataRange();
var min = range.min;
var max = range.max;
var min = range.min !== null ? range.min.valueOf() : null;
var max = range.max !== null ? range.max.valueOf() : null;
var minItem = null;
var maxItem = null;
@ -403,12 +407,13 @@ Timeline.prototype.getItemRange = function () {
// calculate the date of the left side and right side of the items given
util.forEach(this.itemSet.items, function (item) {
item.show();
item.repositionX();
var start = getStart(item);
var end = getEnd(item);
var left = new Date(start - (item.getWidthLeft() + 10) * factor);
var right = new Date(end + (item.getWidthRight() + 10) * factor);
var left = start - (item.getWidthLeft() + 10) * factor;
var right = end + (item.getWidthRight() + 10) * factor;
if (left < min) {
min = left;
@ -455,7 +460,7 @@ Timeline.prototype.getDataRange = function() {
min = start;
}
if (max === null || end > max) {
max = start;
max = end;
}
});
}

+ 2
- 1
lib/timeline/component/CurrentTime.js View File

@ -91,7 +91,7 @@ CurrentTime.prototype.redraw = function() {
var locale = this.options.locales[this.options.locale];
if (!locale) {
if (!this.warned) {
console.log('WARNING: options.locales[\'' + this.options.locale + '\'] not found. See http://visjs.org/docs/timeline.html#Localization');
console.log('WARNING: options.locales[\'' + this.options.locale + '\'] not found. See http://visjs.org/docs/timeline/#Localization');
this.warned = true;
}
locale = this.options.locales['en']; // fall back on english when not available
@ -129,6 +129,7 @@ CurrentTime.prototype.start = function() {
if (interval > 1000) interval = 1000;
me.redraw();
me.body.emitter.emit('currentTimeTick');
// start a renderTimer to adjust for the new time
me.currentTimeTimer = setTimeout(update, interval);

+ 19
- 6
lib/timeline/component/CustomTime.js View File

@ -23,16 +23,17 @@ function CustomTime (body, options) {
moment: moment,
locales: locales,
locale: 'en',
id: undefined
id: undefined,
title: undefined
};
this.options = util.extend({}, this.defaultOptions);
if (options && options.time) {
this.customTime = options.time;
} else {
this.customTime = new Date();
this.customTime = new Date();
}
this.eventParams = {}; // stores state parameters while dragging the bar
this.setOptions(options);
@ -83,7 +84,7 @@ CustomTime.prototype._create = function() {
this.hammer.on('panstart', this._onDragStart.bind(this));
this.hammer.on('panmove', this._onDrag.bind(this));
this.hammer.on('panend', this._onDragEnd.bind(this));
this.hammer.get('pan').set({threshold:5, direction:30}); // 30 is ALL_DIRECTIONS in hammer.
this.hammer.get('pan').set({threshold:5, direction: Hammer.DIRECTION_HORIZONTAL});
};
/**
@ -123,8 +124,12 @@ CustomTime.prototype.redraw = function () {
locale = this.options.locales['en']; // fall back on english when not available
}
var title = locale.time + ': ' + this.options.moment(this.customTime).format('dddd, MMMM Do YYYY, H:mm:ss');
title = title.charAt(0).toUpperCase() + title.substring(1);
var title = this.options.title;
// To hide the title completely use empty string ''.
if (title === undefined) {
title = locale.time + ': ' + this.options.moment(this.customTime).format('dddd, MMMM Do YYYY, H:mm:ss');
title = title.charAt(0).toUpperCase() + title.substring(1);
}
this.bar.style.left = x + 'px';
this.bar.title = title;
@ -159,6 +164,14 @@ CustomTime.prototype.getCustomTime = function() {
return new Date(this.customTime.valueOf());
};
/**
* Set custom title.
* @param {Date | number | string} title
*/
CustomTime.prototype.setCustomTitle = function(title) {
this.options.title = title;
};
/**
* Start moving horizontally
* @param {Event} event

+ 55
- 105
lib/timeline/component/DataAxis.js View File

@ -1,8 +1,7 @@
var util = require('../../util');
var DOMutil = require('../../DOMutil');
var Component = require('./Component');
var DataStep = require('../DataStep');
var DataScale = require('./DataScale');
/**
* A horizontal time axis
* @param {Object} [options] See DataAxis.setOptions for the available
@ -19,7 +18,7 @@ function DataAxis (body, options, svg, linegraphOptions) {
orientation: 'left', // supported: 'left', 'right'
showMinorLabels: true,
showMajorLabels: true,
icons: true,
icons: false,
majorLinesOffset: 7,
minorLinesOffset: 4,
labelOffsetX: 10,
@ -30,12 +29,12 @@ function DataAxis (body, options, svg, linegraphOptions) {
alignZeros: true,
left:{
range: {min:undefined,max:undefined},
format: function (value) {return value;},
format: function (value) {return ''+parseFloat(value.toPrecision(3));},
title: {text:undefined,style:undefined}
},
right:{
range: {min:undefined,max:undefined},
format: function (value) {return value;},
format: function (value) {return ''+parseFloat(value.toPrecision(3));},
title: {text:undefined,style:undefined}
}
};
@ -50,7 +49,7 @@ function DataAxis (body, options, svg, linegraphOptions) {
};
this.dom = {};
this.scale= undefined;
this.range = {start:0, end:0};
this.options = util.extend({}, this.defaultOptions);
@ -59,7 +58,7 @@ function DataAxis (body, options, svg, linegraphOptions) {
this.setOptions(options);
this.width = Number(('' + this.options.width).replace("px",""));
this.minWidth = this.width;
this.height = this.linegraphSVG.offsetHeight;
this.height = this.linegraphSVG.getBoundingClientRect().height;
this.hidden = false;
this.stepPixels = 25;
@ -68,15 +67,16 @@ function DataAxis (body, options, svg, linegraphOptions) {
this.lineOffset = 0;
this.master = true;
this.masterAxis = null;
this.svgElements = {};
this.iconsRemoved = false;
this.groups = {};
this.amountOfGroups = 0;
// create the HTML DOM
this._create();
this.framework = {svg: this.svg, svgElements: this.svgElements, options: this.options, groups: this.groups};
var me = this;
this.body.emitter.on("verticalDrag", function() {
@ -95,6 +95,9 @@ DataAxis.prototype.addGroup = function(label, graphOptions) {
};
DataAxis.prototype.updateGroup = function(label, graphOptions) {
if (!this.groups.hasOwnProperty(label)) {
this.amountOfGroups += 1;
}
this.groups[label] = graphOptions;
};
@ -128,10 +131,9 @@ DataAxis.prototype.setOptions = function (options) {
'right',
'alignZeros'
];
util.selectiveExtend(fields, this.options, options);
util.selectiveDeepExtend(fields, this.options, options);
this.minWidth = Number(('' + this.options.width).replace("px",""));
if (redraw === true && this.dom.frame) {
this.hide();
this.show();
@ -187,7 +189,7 @@ DataAxis.prototype._redrawGroupIcons = function () {
for (var i = 0; i < groupArray.length; i++) {
var groupId = groupArray[i];
if (this.groups[groupId].visible === true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] === true)) {
this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight);
this.groups[groupId].getLegend(iconWidth, iconHeight, this.framework, x, y);
y += iconHeight + iconOffset;
}
}
@ -244,11 +246,6 @@ DataAxis.prototype.hide = function() {
* @param end
*/
DataAxis.prototype.setRange = function (start, end) {
if (this.master === false && this.options.alignZeros === true && this.zeroCrossing != -1) {
if (start > 0) {
start = 0;
}
}
this.range.start = start;
this.range.end = end;
};
@ -260,7 +257,7 @@ DataAxis.prototype.setRange = function (start, end) {
DataAxis.prototype.redraw = function () {
var resized = false;
var activeGroups = 0;
// Make sure the line container adheres to the vertical scrolling.
this.dom.lineContainer.style.top = this.body.domProps.scrollTop + 'px';
@ -348,105 +345,60 @@ DataAxis.prototype._redrawLabels = function () {
DOMutil.prepareElements(this.DOMelements.lines);
DOMutil.prepareElements(this.DOMelements.labels);
var orientation = this.options['orientation'];
var customRange = this.options[orientation].range != undefined? this.options[orientation].range:{};
// get the range for the slaved axis
var step;
if (this.master === false) {
var stepSize, rangeStart, rangeEnd, minimumStep;
if (this.zeroCrossing !== -1 && this.options.alignZeros === true) {
if (this.range.end > 0) {
stepSize = this.range.end / this.zeroCrossing; // size of one step
rangeStart = this.range.end - this.amountOfSteps * stepSize;
rangeEnd = this.range.end;
}
else {
// all of the range (including start) has to be done before the zero crossing.
stepSize = -1 * this.range.start / (this.amountOfSteps - this.zeroCrossing); // absolute size of a step
rangeStart = this.range.start;
rangeEnd = this.range.start + stepSize * this.amountOfSteps;
}
}
else {
rangeStart = this.range.start;
rangeEnd = this.range.end;
}
minimumStep = this.stepPixels;
//Override range with manual options:
var autoScaleEnd = true;
if (customRange.max != undefined){
this.range.end = customRange.max;
autoScaleEnd = false;
}
else {
// calculate range and step (step such that we have space for 7 characters per label)
minimumStep = this.props.majorCharHeight;
rangeStart = this.range.start;
rangeEnd = this.range.end;
var autoScaleStart = true;
if (customRange.min != undefined){
this.range.start = customRange.min;
autoScaleStart = false;
}
this.step = step = new DataStep(
rangeStart,
rangeEnd,
minimumStep,
this.scale = new DataScale(
this.range.start,
this.range.end,
autoScaleStart,
autoScaleEnd,
this.dom.frame.offsetHeight,
this.options[this.options.orientation].range,
this.options[this.options.orientation].format,
this.master === false && this.options.alignZeros // does the step have to align zeros? only if not master and the options is on
this.props.majorCharHeight,
this.options.alignZeros,
this.options[orientation].format
);
// the slave axis needs to use the same horizontal lines as the master axis.
if (this.master === true) {
this.stepPixels = ((this.dom.frame.offsetHeight) / step.marginRange) * step.step;
this.amountOfSteps = Math.ceil(this.dom.frame.offsetHeight / this.stepPixels);
if (this.master === false && this.masterAxis != undefined){
this.scale.followScale(this.masterAxis.scale);
}
else {
// align with zero
if (this.options.alignZeros === true && this.zeroCrossing !== -1) {
// distance is the amount of steps away from the zero crossing we are.
let distance = (step.current - this.zeroCrossing * step.step) / step.step;
this.step.shift(distance);
}
}
// value at the bottom of the SVG
this.valueAtBottom = step.marginEnd;
//Is updated in side-effect of _redrawLabel():
this.maxLabelSize = 0;
var y = 0; // init value
var stepIndex = 0; // init value
var isMajor = false; // init value
while (stepIndex < this.amountOfSteps) {
y = Math.round(stepIndex * this.stepPixels);
isMajor = step.isMajor();
if (stepIndex > 0 && stepIndex !== this.amountOfSteps) {
if (this.options['showMinorLabels'] && isMajor === false || this.master === false && this.options['showMinorLabels'] === true) {
this._redrawLabel(y - 2, step.getCurrent(), orientation, 'vis-y-axis vis-minor', this.props.minorCharHeight);
}
if (isMajor && this.options['showMajorLabels'] && this.master === true ||
this.options['showMinorLabels'] === false && this.master === false && isMajor === true) {
var lines = this.scale.getLines();
lines.forEach(
line=> {
var y = line.y;
var isMajor = line.major;
if (this.options['showMinorLabels'] && isMajor === false) {
this._redrawLabel(y - 2, line.val, orientation, 'vis-y-axis vis-minor', this.props.minorCharHeight);
}
if (isMajor) {
if (y >= 0) {
this._redrawLabel(y - 2, step.getCurrent(), orientation, 'vis-y-axis vis-major', this.props.majorCharHeight);
this._redrawLabel(y - 2, line.val, orientation, 'vis-y-axis vis-major', this.props.majorCharHeight);
}
this._redrawLine(y, orientation, 'vis-grid vis-horizontal vis-major', this.options.majorLinesOffset, this.props.majorLineWidth);
}
else {
this._redrawLine(y, orientation, 'vis-grid vis-horizontal vis-minor', this.options.minorLinesOffset, this.props.minorLineWidth);
if (this.master === true) {
if (isMajor) {
this._redrawLine(y, orientation, 'vis-grid vis-horizontal vis-major', this.options.majorLinesOffset, this.props.majorLineWidth);
}
else {
this._redrawLine(y, orientation, 'vis-grid vis-horizontal vis-minor', this.options.minorLinesOffset, this.props.minorLineWidth);
}
}
}
// get zero crossing
if (this.master === true && step.current === 0) {
this.zeroCrossing = stepIndex;
}
step.next();
stepIndex += 1;
}
// get zero crossing if it's the last step
if (this.master === true && step.current === 0) {
this.zeroCrossing = stepIndex;
}
this.conversionFactor = this.stepPixels / step.step;
});
// Note that title is rotated, so we're using the height, not width!
var titleWidth = 0;
@ -483,13 +435,11 @@ DataAxis.prototype._redrawLabels = function () {
};
DataAxis.prototype.convertValue = function (value) {
var invertedValue = this.valueAtBottom - value;
var convertedValue = invertedValue * this.conversionFactor;
return convertedValue;
return this.scale.convertValue(value);
};
DataAxis.prototype.screenToValue = function (x) {
return this.valueAtBottom - (x / this.conversionFactor);
return this.scale.screenToValue(x);
};
/**

+ 237
- 0
lib/timeline/component/DataScale.js View File

@ -0,0 +1,237 @@
/**
* Created by ludo on 25-1-16.
*/
function DataScale(start, end, autoScaleStart, autoScaleEnd, containerHeight, majorCharHeight, zeroAlign = false, formattingFunction=false) {
this.majorSteps = [1, 2, 5, 10];
this.minorSteps = [0.25, 0.5, 1, 2];
this.customLines = null;
this.containerHeight = containerHeight;
this.majorCharHeight = majorCharHeight;
this._start = start;
this._end = end;
this.scale = 1;
this.minorStepIdx = -1;
this.magnitudefactor = 1;
this.determineScale();
this.zeroAlign = zeroAlign;
this.autoScaleStart = autoScaleStart;
this.autoScaleEnd = autoScaleEnd;
this.formattingFunction = formattingFunction;
if (autoScaleStart || autoScaleEnd) {
var me = this;
var roundToMinor = function (value) {
var rounded = value - (value % (me.magnitudefactor * me.minorSteps[me.minorStepIdx]));
if (value % (me.magnitudefactor * me.minorSteps[me.minorStepIdx]) > 0.5 * (me.magnitudefactor * me.minorSteps[me.minorStepIdx])) {
return rounded + (me.magnitudefactor * me.minorSteps[me.minorStepIdx]);
}
else {
return rounded;
}
};
if (autoScaleStart) {
this._start -= this.magnitudefactor * 2 * this.minorSteps[this.minorStepIdx];
this._start = roundToMinor(this._start);
}
if (autoScaleEnd) {
this._end += this.magnitudefactor * this.minorSteps[this.minorStepIdx];
this._end = roundToMinor(this._end);
}
this.determineScale();
}
}
DataScale.prototype.setCharHeight = function (majorCharHeight) {
this.majorCharHeight = majorCharHeight;
};
DataScale.prototype.setHeight = function (containerHeight) {
this.containerHeight = containerHeight;
};
DataScale.prototype.determineScale = function () {
var range = this._end - this._start;
this.scale = this.containerHeight / range;
var minimumStepValue = this.majorCharHeight / this.scale;
var orderOfMagnitude = (range > 0)
? Math.round(Math.log(range) / Math.LN10)
: 0;
this.minorStepIdx = -1;
this.magnitudefactor = Math.pow(10, orderOfMagnitude);
var start = 0;
if (orderOfMagnitude < 0) {
start = orderOfMagnitude;
}
var solutionFound = false;
for (var l = start; Math.abs(l) <= Math.abs(orderOfMagnitude); l++) {
this.magnitudefactor = Math.pow(10, l);
for (var j = 0; j < this.minorSteps.length; j++) {
var stepSize = this.magnitudefactor * this.minorSteps[j];
if (stepSize >= minimumStepValue) {
solutionFound = true;
this.minorStepIdx = j;
break;
}
}
if (solutionFound === true) {
break;
}
}
};
DataScale.prototype.is_major = function (value) {
return (value % (this.magnitudefactor * this.majorSteps[this.minorStepIdx]) === 0);
};
DataScale.prototype.getStep = function(){
return this.magnitudefactor * this.minorSteps[this.minorStepIdx];
};
DataScale.prototype.getFirstMajor = function(){
var majorStep = this.magnitudefactor * this.majorSteps[this.minorStepIdx];
return this.convertValue(this._start + ((majorStep - (this._start % majorStep)) % majorStep));
};
DataScale.prototype.formatValue = function(current) {
var returnValue = current.toPrecision(5);
if (typeof this.formattingFunction === 'function') {
returnValue = this.formattingFunction(current);
}
if (typeof returnValue === 'number') {
return '' + returnValue;
}
else if (typeof returnValue === 'string') {
return returnValue;
}
else {
return current.toPrecision(5);
}
};
DataScale.prototype.getLines = function () {
var lines = [];
var step = this.getStep();
var bottomOffset = (step - (this._start % step)) % step;
for (var i = (this._start + bottomOffset); this._end-i > 0.00001; i += step) {
if (i != this._start) { //Skip the bottom line
lines.push({major: this.is_major(i), y: this.convertValue(i), val: this.formatValue(i)});
}
}
return lines;
};
DataScale.prototype.followScale = function (other) {
var oldStepIdx = this.minorStepIdx;
var oldStart = this._start;
var oldEnd = this._end;
var me = this;
var increaseMagnitude = function () {
me.magnitudefactor *= 2;
};
var decreaseMagnitude = function () {
me.magnitudefactor /= 2;
};
if ((other.minorStepIdx <= 1 && this.minorStepIdx <= 1) || (other.minorStepIdx > 1 && this.minorStepIdx > 1)) {
//easy, no need to change stepIdx nor multiplication factor
} else if (other.minorStepIdx < this.minorStepIdx) {
//I'm 5, they are 4 per major.
this.minorStepIdx = 1;
if (oldStepIdx == 2) {
increaseMagnitude();
} else {
increaseMagnitude();
increaseMagnitude();
}
} else {
//I'm 4, they are 5 per major
this.minorStepIdx = 2;
if (oldStepIdx == 1) {
decreaseMagnitude();
} else {
decreaseMagnitude();
decreaseMagnitude();
}
}
//Get masters stats:
var lines = other.getLines();
var otherZero = other.convertValue(0);
var otherStep = other.getStep() * other.scale;
var done = false;
var count = 0;
//Loop until magnitude is correct for given constrains.
while (!done && count++ <5) {
//Get my stats:
this.scale = otherStep / (this.minorSteps[this.minorStepIdx] * this.magnitudefactor);
var newRange = this.containerHeight / this.scale;
//For the case the magnitudefactor has changed:
this._start = oldStart;
this._end = this._start + newRange;
var myOriginalZero = this._end * this.scale;
var majorStep = this.magnitudefactor * this.majorSteps[this.minorStepIdx];
var majorOffset = this.getFirstMajor() - other.getFirstMajor();
if (this.zeroAlign) {
var zeroOffset = otherZero - myOriginalZero;
this._end += (zeroOffset / this.scale);
this._start = this._end - newRange;
} else {
if (!this.autoScaleStart) {
this._start += majorStep - (majorOffset / this.scale);
this._end = this._start + newRange;
} else {
this._start -= majorOffset / this.scale;
this._end = this._start + newRange;
}
}
if (!this.autoScaleEnd && this._end > oldEnd+0.00001) {
//Need to decrease magnitude to prevent scale overshoot! (end)
decreaseMagnitude();
done = false;
continue;
}
if (!this.autoScaleStart && this._start < oldStart-0.00001) {
if (this.zeroAlign && oldStart >= 0) {
console.warn("Can't adhere to given 'min' range, due to zeroalign");
} else {
//Need to decrease magnitude to prevent scale overshoot! (start)
decreaseMagnitude();
done = false;
continue;
}
}
if (this.autoScaleStart && this.autoScaleEnd && newRange < (oldEnd-oldStart)){
increaseMagnitude();
done = false;
continue;
}
done = true;
}
};
DataScale.prototype.convertValue = function (value) {
return this.containerHeight - ((value - this._start) * this.scale);
};
DataScale.prototype.screenToValue = function (pixels) {
return ((this.containerHeight - pixels) / this.scale) + this._start;
};
module.exports = DataScale;

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save