Browse Source

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

revert-3409-performance
Yotam Berkowitz 7 years ago
parent
commit
73423c075b
39 changed files with 1464 additions and 808 deletions
  1. +2
    -1
      .gitignore
  2. +62
    -0
      HISTORY.md
  3. +126
    -54
      docs/graph3d/index.html
  4. +17
    -6
      docs/network/index.html
  5. +4
    -1
      docs/network/nodes.html
  6. +21
    -2
      docs/timeline/index.html
  7. +38
    -0
      examples/timeline/editing/itemsAlwaysDraggable.html
  8. +307
    -0
      lib/graph3d/DataGroup.js
  9. +4
    -9
      lib/graph3d/Filter.js
  10. +91
    -277
      lib/graph3d/Graph3d.js
  11. +4
    -0
      lib/network/CachedImage.js
  12. +15
    -1
      lib/network/Network.js
  13. +30
    -2
      lib/network/dotparser.js
  14. +51
    -7
      lib/network/modules/Canvas.js
  15. +9
    -23
      lib/network/modules/CanvasRenderer.js
  16. +14
    -4
      lib/network/modules/Clustering.js
  17. +16
    -4
      lib/network/modules/EdgesHandler.js
  18. +42
    -56
      lib/network/modules/InteractionHandler.js
  19. +259
    -147
      lib/network/modules/LayoutEngine.js
  20. +11
    -3
      lib/network/modules/NodesHandler.js
  21. +14
    -3
      lib/network/modules/PhysicsEngine.js
  22. +8
    -4
      lib/network/modules/components/Edge.js
  23. +22
    -0
      lib/network/modules/components/Node.js
  24. +1
    -14
      lib/network/modules/components/edges/BezierEdgeDynamic.js
  25. +62
    -136
      lib/network/modules/components/edges/BezierEdgeStatic.js
  26. +2
    -17
      lib/network/modules/components/edges/CubicBezierEdge.js
  27. +42
    -1
      lib/network/modules/components/edges/util/BezierEdgeBase.js
  28. +5
    -9
      lib/network/modules/components/shared/Label.js
  29. +7
    -2
      lib/timeline/Range.js
  30. +5
    -1
      lib/timeline/TimeStep.js
  31. +1
    -0
      lib/timeline/component/Group.js
  32. +21
    -4
      lib/timeline/component/ItemSet.js
  33. +7
    -2
      lib/timeline/component/item/Item.js
  34. +6
    -5
      lib/timeline/component/item/RangeItem.js
  35. +5
    -1
      lib/timeline/optionsTimeline.js
  36. +12
    -11
      lib/util.js
  37. +1
    -1
      package.json
  38. +46
    -0
      test/TimeStep.test.js
  39. +74
    -0
      test/dotparser.test.js

+ 2
- 1
.gitignore View File

@ -13,5 +13,6 @@ npm-debug.log
.settings/
.directory
# vim temporary files
# temporary files
.*.sw[op]
.commits.tmp

+ 62
- 0
HISTORY.md View File

@ -1,6 +1,68 @@
# vis.js history
http://visjs.org
## 2017-05-21, version 4.20.0
### General
- FIX #2934: Replacing all ES6 imports with CJS require calls (#3063)
- Add command line options to mocha for running tests (#3064)
- Added documentation on how labels are used (#2873)
- FIX: Fix typo in PR template (#2908)
- FIX #2912: updated moment.js (#2925)
- Added @wimrijnders to the support team (#2886)
### Network
- FIX: Fixes for loading images into image nodes (#2964)
- FIX #3025: Added check on mission var 'options', refactoring. (#3055)
- FIX #3057: Use get() to get data from DataSet/View instead of directly accessing member \_data. (#3069)
- FIX #3065: Avoid overriding standard context method ellipse() (#3072)
- FIX #2922: bold label for selected ShapeBase classes (#2924)
- FIX #2952: Pre-render node images for interpolation (#3010)
- FIX #1735: Fix for exploding directed network, first working version; refactored hierarchical state in LayoutEngine.(#3017)
- Refactoring of Label.propagateFonts() (#3052)
- FIX #2894: Set CircleImageBase.imageObjAlt always when options change (#3053)
- FIX #3047: Label.getFormattingValues() fix option fallback to main font for mod-fonts (#3054)
- FIX #2938: Fix handling of node id's in saveAndLoad example (#2943)
- FIX: Refactoring in Canvas.js (#3030)
- FIX #2968: Fix placement label for dot shape (#3018)
- FIX #2994: select edge with id zero (#2996)
- FIX #1847, #2436: Network: use separate refresh indicator in NodeBase, instead of width… (#2885)
- Fix #2914: Use option edges.chosen if present in global options (#2917)
- FIX #2940: Gephi consolidate double assignment of node title (#2962)
- FIX 2936: Fix check for nodes not present in EdgesHandler (#2963)
- FEAT: Reduce the time-complexity of the network initial positioning (#2759)
### Timeline / Graph2D
- FEAT: Add support for multiple class names in utils add/remove class methods (#3079)
- FEAT: Adds 'showTooltips' option to override popups displayed for items with titles (#3046)
- FIX #2818: LineGraph: Add an existingItemsMap to check if items are new or not before skipping (#3075)
- FEAT #2835: Improve timeline stack performance (#2848, #3078)
- FIX #3032: mouseup and mousedown events (#3059)
- FIX #2421: Fix click and doubleclick events on items (#2988)
- FEAT #1405, #1715, #3002: Implementation of a week scale feature (#3009)
- FIX #397: Eliminate repeatedly fired `rangechanged` events on mousewheel (#2989)
- FIX #2939: Add check for parent existence when changing group in Item.setData (#2985)
- FIX #2877: Add check for empty groupIds array and get full list from data set (#2986)
- FIX #2614: Timeline docs border overlaps (#2992)
- FIX: Doubleclick add (#2987)
- FIX #2679: Cannot read property 'hasOwnProperty' of null (#2973)
- FEAT #2863: Drag and drop custom fields (#2872)
- FEAT #2834: Control over the drop event (#2974)
- FIX #2918: Remove usages of elementsCensor (#2947)
- FEAT #2948: Rolling mode offset (#2950)
- FEAT #2805: Add callback functions to moveTo, zoomIn, zoomOut and setWindow (#2870)
- FIX: Do not corrupt class names at high zoom levels (#2909)
- FIX #2888: Fix error in class names (#2911)
- FIX #2835: Visible items bug (#2878)
### Graph3D
- FEAT: Configurable minimum and maximum sizes for dot-size graphs (#2849)
## 2017-03-19, version 4.19.1
### General

+ 126
- 54
docs/graph3d/index.html View File

@ -272,7 +272,7 @@ var options = {
The following options are available.
</p>
<table class="options">
<table class="options" id="optionTable">
<tr>
<th>Name</th>
<th>Type</th>
@ -311,69 +311,83 @@ var options = {
<td>The color of the axis lines and the text along the axis.</td>
</tr>
<tr>
<td>backgroundColor</td>
<tr class='toggle collapsible' onclick="toggleTable('optionTable','backgroundColor', this);">
<td><span parent="backgroundColor" class="right-caret"></span> backgroundColor</td>
<td>string or Object</td>
<td>{fill:&nbsp;'white', stroke:&nbsp;'gray', strokeWidth:&nbsp;1}</td>
<td>Object</td>
<td>The background color for the main area of the chart.
Can be either a simple HTML color string, for example: 'red' or '#00cc00',
or an object with the following properties.</td>
</tr>
<tr>
<td>backgroundColor.fill</td>
<tr parent="backgroundColor" class="hidden">
<td class="indent">backgroundColor.fill</td>
<td>string</td>
<td>'white'</td>
<td>The chart fill color, as an HTML color string.</td>
</tr>
<tr>
<td>backgroundColor.stroke</td>
<tr parent="backgroundColor" class="hidden">
<td class="indent">backgroundColor.stroke</td>
<td>string</td>
<td>'gray'</td>
<td>The color of the chart border, as an HTML color string.</td>
</tr>
<tr>
<td>backgroundColor.strokeWidth</td>
<tr parent="backgroundColor" class="hidden">
<td class="indent">backgroundColor.strokeWidth</td>
<td>number</td>
<td>1</td>
<td>The border width, in pixels.</td>
</tr>
<tr>
<td>cameraPosition</td>
<tr class='toggle collapsible' onclick="toggleTable('optionTable','cameraPosition', this);">
<td><span parent="cameraPosition" class="right-caret"></span> cameraPosition</td>
<td>Object</td>
<td>Object</td>
<td>{horizontal:&nbsp;1.0, vertical:&nbsp;0.5, distance:&nbsp;1.7}</td>
<td>Set the initial rotation and position of the camera.
The object <code>cameraPosition</code> contains three parameters:
<code>horizontal</code>, <code>vertical</code>, and <code>distance</code>.
Parameter <code>horizontal</code> is a value in radians and can have any
value (but normally in the range of 0 and 2*Pi).
Parameter <code>vertical</code> is a value in radians between 0 and 0.5*Pi.
Parameter <code>distance</code> is the (normalized) distance from the
camera to the center of the graph, in the range of 0.71 to 5.0. A
larger distance puts the graph further away, making it smaller.
All parameters are optional.
</tr>
<tr parent="cameraPosition" class="hidden">
<td class="indent">cameraPosition.horizontal</td>
<td>number</td>
<td>1.0</td>
<td>Value in radians. It can have any
value, but is normally in the range of 0 and 2*Pi.</td>
</tr>
<tr parent="cameraPosition" class="hidden">
<td class="indent">cameraPosition.vertical</td>
<td>number</td>
<td>0.5</td>
<td>Value in radians between 0 and 0.5*Pi.</td>
</tr>
<tr parent="cameraPosition" class="hidden">
<td class="indent">cameraPosition.distance</td>
<td>number</td>
<td>1.7</td>
<td>The (normalized) distance from the
camera to the center of the graph, in the range of 0.71 to 5.0. A
larger distance puts the graph further away, making it smaller.</p>
</tr>
<tr>
<td>dataColor</td>
<tr class='toggle collapsible' onclick="toggleTable('optionTable','dataColor', this);">
<td><span parent="dataColor" class="right-caret"></span> dataColor</td>
<td>string or object</td>
<td>{fill:&nbsp;'#7DC1FF', stroke:&nbsp;'#3267D2', strokeWidth:&nbsp;1}</td>
<td>Object</td>
<td>When <code>dataColor</code> is a string, it will set the color for both border and fill color of dots and bars. Applicable for styles <code>dot-size</code>, <code>bar-size</code>, and <code>line</code>. When an object, it can contain the properties descibed below.</td>
</tr>
<tr>
<td>dataColor.fill</td>
<tr parent="dataColor" class="hidden">
<td class="indent">dataColor.fill</td>
<td>string</td>
<td>'#7DC1FF'</td>
<td>The fill color of the dots or bars. Applicable when using styles <code>dot-size</code>, <code>bar-size</code>, or <code>line</code>.</td>
</tr>
<tr>
<td>dataColor.stroke</td>
<tr parent="dataColor" class="hidden">
<td class="indent">dataColor.stroke</td>
<td>string</td>
<td>'#3267D2'</td>
<td>The border color of the dots or bars. Applicable when using styles <code>dot-size</code> or <code>bar-size</code>.</td>
</tr>
<tr>
<td>dataColor.strokeWidth</td>
<tr parent="dataColor" class="hidden">
<td class="indent">dataColor.strokeWidth</td>
<td>number</td>
<td>1</td>
<td>The line width of dots, bars and lines. Applicable for all styles.</td>
@ -516,37 +530,95 @@ var options = {
</td>
</tr>
<tr>
<td>tooltipStyle</td>
<tr class='toggle collapsible' onclick="toggleTable('optionTable','tooltipStyle', this);">
<td><span parent="tooltipStyle" class="right-caret"></span> tooltipStyle</td>
<td>Object</td>
<td>Object</td>
<td>
<pre class="prettyprint lang-js">
{
content: {
padding: '10px',
border: '1px solid #4d4d4d',
color: '#1a1a1a',
background: 'rgba(255,255,255,0.7)',
borderRadius: '2px',
boxShadow: '5px 5px 10px rgba(128,128,128,0.5)'
},
line: {
height: '40px',
width: '0',
borderLeft: '1px solid #4d4d4d'
},
dot: {
height: '0',
width: '0',
border: '5px solid #4d4d4d',
borderRadius: '5px'
}
}</pre>
</td>
<td>Tooltip style properties.
Provided properties will be merged with the default object.
</td>
</tr>
<!-- Can't define separate entries for content, line and dot objects here,
because toggleTable() can't handle multiple levels of collapsibles -->
<tr parent="tooltipStyle" class="hidden">
<td class="indent">tooltipStyle.content.padding</td>
<td>string</td>
<td>'10px'</td>
<td></td>
</tr>
<tr parent="tooltipStyle" class="hidden">
<td class="indent">tooltipStyle.content.border</td>
<td>string</td>
<td>'1px solid #4d4d4d'</td>
<td></td>
</tr>
<tr parent="tooltipStyle" class="hidden">
<td class="indent">tooltipStyle.content.color</td>
<td>string</td>
<td>'#1a1a1a'</td>
<td></td>
</tr>
<tr parent="tooltipStyle" class="hidden">
<td class="indent">tooltipStyle.content.background</td>
<td>string</td>
<td>'rgba(255,255,255,0.7)'</td>
<td></td>
</tr>
<tr parent="tooltipStyle" class="hidden">
<td class="indent">tooltipStyle.content.borderRadius</td>
<td>string</td>
<td>'2px'</td>
<td></td>
</tr>
<tr parent="tooltipStyle" class="hidden">
<td class="indent">tooltipStyle.content.boxShadow</td>
<td>string</td>
<td>'5px 5px 10px rgba(128,128,128,0.5)'</td>
<td></td>
</tr>
<tr parent="tooltipStyle" class="hidden">
<td class="indent">tooltipStyle.line.height</td>
<td>string</td>
<td>'40px'</td>
<td></td>
</tr>
<tr parent="tooltipStyle" class="hidden">
<td class="indent">tooltipStyle.line.width</td>
<td>string</td>
<td>'0'</td>
<td></td>
</tr>
<tr parent="tooltipStyle" class="hidden">
<td class="indent">tooltipStyle.line.borderLeft</td>
<td>string</td>
<td>'1px solid #4d4d4d'</td>
<td></td>
</tr>
<tr parent="tooltipStyle" class="hidden">
<td class="indent">tooltipStyle.dot.height</td>
<td>string</td>
<td>'0'</td>
<td></td>
</tr>
<tr parent="tooltipStyle" class="hidden">
<td class="indent">tooltipStyle.dot.width</td>
<td>string</td>
<td>'0'</td>
<td></td>
</tr>
<tr parent="tooltipStyle" class="hidden">
<td class="indent">tooltipStyle.dot.border</td>
<td>string</td>
<td>'5px solid #4d4d4d'</td>
<td></td>
</tr>
<tr parent="tooltipStyle" class="hidden">
<td class="indent">tooltipStyle.dot.borderRadius</td>
<td>string</td>
<td>'5px'</td>
<td></td>
</tr>
<tr>
<td>valueMax</td>

+ 17
- 6
docs/network/index.html View File

@ -611,13 +611,18 @@ var locales = {
</tr>
<tr class="collapsible toggle" onclick="toggleTable('methodTable','findNode', this);">
<td colspan="2"><span parent="findNode" class="right-caret" id="method_findNode"></span> findNode(
<code>String nodeId</code>)
<code>String/Number nodeId</code>)
</tr>
<tr class="hidden" parent="findNode">
<td class="midMethods">Returns: Array</td>
<td>Nodes can be in clusters. Clusters can also be in clusters. This function returns and array of
nodeIds
showing where the node is. <br><br> Example:
nodeIds showing where the node is.
<br><br>
If any nodeId in the chain, especially the first passed in as a parameter, is not present in
the current nodes list, an empty array is returned.
<br><br> Example:
cluster 'A' contains cluster 'B',
cluster 'B' contains cluster 'C',
cluster 'C' contains node 'fred'.
@ -866,13 +871,19 @@ function releaseFunction (clusterPosition, containedNodesPositions) {
</tr>
<tr class="collapsible toggle" onclick="toggleTable('methodTable','getConnectedNodes', this);">
<td colspan="2"><span parent="getConnectedNodes" class="right-caret" id="method_getConnectedNodes"></span> getConnectedNodes(<code><i>String
nodeId or edgeId</i></code>)
nodeId or edgeId, [String direction]</i></code>)
</td>
</tr>
<tr class="hidden" parent="getConnectedNodes">
<td class="midMethods">Returns: Array</td>
<td>Returns an array of nodeIds of the all the nodes that are directly connected to this node. If you supply an edgeId,
vis will first match the id to nodes. If no match is found, it will search in the edgelist and return an array: <code>[fromId, toId]</code>.</td>
<td>Returns an array of nodeIds of all the nodes that are directly connected to this node or edge.<br><br>
For a node id, returns an array with the id's of the connected nodes.<br>
If optional parameter <code>direction</code> is set to string <i>'from'</i>, only parent nodes are returned.<br>
If <code>direction</code> is set to <i>'to'</i>, only child nodes are returned.<br>
Any other value or <code>undefined</code> returns both parent and child nodes.
<br><br>
For an edge id, returns an array: <code>[fromId, toId]</code>.
Parameter <i>direction</i> is ignored for edges.</td>
</tr>
<tr class="collapsible toggle" onclick="toggleTable('methodTable','getConnectedEdges', this);">
<td colspan="2"><span parent="getConnectedEdges" class="right-caret" id="method_getConnectedEdges"></span> getConnectedEdges(<code><i>String

+ 4
- 1
docs/network/nodes.html View File

@ -809,7 +809,10 @@ network.setOptions(options);
<td>Number</td>
<td><code>1</code></td>
<td>The barnesHut physics model (which is enabled by default) is based on an inverted gravity model. By
increasing the mass of a node, you increase it's repulsion. Values lower than 1 are not recommended.
increasing the mass of a node, you increase it's repulsion.
<br><br>
Values between 0 and 1 are not recommended.<br>
Negative or zero values are not allowed. These will generate a console error and will be set to 1.
</td>
</tr>
<tr>

+ 21
- 2
docs/timeline/index.html View File

@ -261,6 +261,13 @@ var items = new vis.DataSet([
<a href="#Styles">Styles</a>.
</td>
</tr>
<tr>
<td>align</td>
<td>String</td>
<td>no</td>
<td>This field is optional. If set this overrides the global <code>align</code> configuration option for this item.
</td>
</tr>
<tr>
<td>content</td>
<td>String</td>
@ -729,12 +736,24 @@ function (option, path) {
</td>
</tr>
<tr>
<td>itemsAlwaysDraggable</td>
<tr class='toggle collapsible' onclick="toggleTable('optionTable','itemsAlwaysDraggable', this);">
<td><span parent="itemsAlwaysDraggable" class="right-caret"></span> itemsAlwaysDraggable</td>
<td>boolean or Object</td>
<td>Object</td>
<td>When a boolean, applies the value only to <code>itemsAlwaysDraggable.item</code>.</td>
</tr>
<tr parent="itemsAlwaysDraggable" class="hidden">
<td class="indent">itemsAlwaysDraggable.item</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 parent="itemsAlwaysDraggable" class="hidden">
<td class="indent">itemsAlwaysDraggable.range</td>
<td>boolean</td>
<td><code>false</code></td>
<td>If true, range of all items in the Timeline is draggable without being selected. If false, range is only draggable for the selected item(s). Only applicable when option <code>itemsAlwaysDraggable.item</code> is set <code>true</code>. </td>
</tr>
<tr>
<td>locale</td>

+ 38
- 0
examples/timeline/editing/itemsAlwaysDraggable.html View File

@ -0,0 +1,38 @@
<html>
<head>
<title>Timeline | itemsAlwaysDraggable Option</title>
<meta charset="utf-8">
<script src="../../../dist/vis.js"></script>
<link href="../../../dist/vis-timeline-graph2d.min.css" rel="stylesheet" type="text/css" />
</head>
<body>
<p>The <code>itemsAlwaysDraggable</code> option allows to drag items around without first selecting them. When <code>itemsAlwaysDraggable.range</code> is set to <code>true</code>, the range can be changed without selection as well.</p>
<div id="mytimeline"></div>
<script>
var container = document.getElementById('mytimeline'),
items = new vis.DataSet();
for (var i = 10; i >= 0; i--) {
var start = new Date(new Date().getTime() + i * 100000);
items.add({
id: i,
content: "item " + i,
start: start,
end: new Date(start.getTime() + 100000)
});
}
var options = {
start: new Date(),
end: new Date(new Date().getTime() + 1000000),
editable: true,
itemsAlwaysDraggable: {
item: true,
range: true
}
};
var timeline = new vis.Timeline(container, items, null, options);
</script>
</body>
</html>

+ 307
- 0
lib/graph3d/DataGroup.js View File

@ -0,0 +1,307 @@
var DataSet = require('../DataSet');
var DataView = require('../DataView');
var Point3d = require('./Point3d');
var Range = require('./Range');
/**
* Creates a container for all data of one specific 3D-graph.
*
* On construction, the container is totally empty; the data
* needs to be initialized with method initializeData().
* Failure to do so will result in the following exception begin thrown
* on instantiation of Graph3D:
*
* Error: Array, DataSet, or DataView expected
*
* @constructor
*/
function DataGroup() {
this.dataTable = null; // The original data table
}
/**
* Initializes the instance from the passed data.
*
* Calculates minimum and maximum values and column index values.
*
* The graph3d instance is used internally to access the settings for
* the given instance.
* TODO: Pass settings only instead.
*
* @param {Graph3D} graph3d Reference to the calling Graph3D instance.
* @param {Array | DataSet | DataView} rawData The data containing the items for
* the Graph.
* @param {Number} style Style Number
*/
DataGroup.prototype.initializeData = function(graph3d, rawData, style) {
var me = this;
// unsubscribe from the dataTable
if (this.dataSet) {
this.dataSet.off('*', this._onChange);
}
if (rawData === undefined)
return;
if (Array.isArray(rawData)) {
rawData = new DataSet(rawData);
}
var data;
if (rawData instanceof DataSet || rawData instanceof DataView) {
data = rawData.get();
}
else {
throw new Error('Array, DataSet, or DataView expected');
}
if (data.length == 0)
return;
this.dataSet = rawData;
this.dataTable = data;
// subscribe to changes in the dataset
this._onChange = function () {
me.setData(me.dataSet);
};
this.dataSet.on('*', this._onChange);
// determine the location of x,y,z,value,filter columns
this.colX = 'x';
this.colY = 'y';
this.colZ = 'z';
var withBars = graph3d.hasBars(style);
// determine barWidth from data
if (withBars) {
if (graph3d.defaultXBarWidth !== undefined) {
this.xBarWidth = graph3d.defaultXBarWidth;
}
else {
this.xBarWidth = this.getSmallestDifference(data, this.colX) || 1;
}
if (graph3d.defaultYBarWidth !== undefined) {
this.yBarWidth = graph3d.defaultYBarWidth;
}
else {
this.yBarWidth = this.getSmallestDifference(data, this.colY) || 1;
}
}
// calculate minima and maxima
this._initializeRange(data, this.colX, graph3d, withBars);
this._initializeRange(data, this.colY, graph3d, withBars);
this._initializeRange(data, this.colZ, graph3d, false);
if (data[0].hasOwnProperty('style')) {
this.colValue = 'style';
var valueRange = this.getColumnRange(data, this.colValue);
this._setRangeDefaults(valueRange, graph3d.defaultValueMin, graph3d.defaultValueMax);
this.valueRange = valueRange;
}
};
/**
* Collect the range settings for the given data column.
*
* This internal method is intended to make the range
* initalization more generic.
*
* TODO: if/when combined settings per axis defined, get rid of this.
*
* @private
*
* @param {'x'|'y'|'z'} column The data column to process
* @param {Graph3D} graph3d Reference to the calling Graph3D instance;
* required for access to settings
*/
DataGroup.prototype._collectRangeSettings = function(column, graph3d) {
var index = ['x', 'y', 'z'].indexOf(column);
if (index == -1) {
throw new Error('Column \'' + column + '\' invalid');
}
var upper = column.toUpperCase();
return {
barWidth : this[column + 'BarWidth'],
min : graph3d['default' + upper + 'Min'],
max : graph3d['default' + upper + 'Max'],
step : graph3d['default' + upper + 'Step'],
range_label: column + 'Range', // Name of instance field to write to
step_label : column + 'Step' // Name of instance field to write to
};
}
/**
* Initializes the settings per given column.
*
* TODO: if/when combined settings per axis defined, rewrite this.
*
* @private
*
* @param {DataSet | DataView} data The data containing the items for the Graph
* @param {'x'|'y'|'z'} column The data column to process
* @param {Graph3D} graph3d Reference to the calling Graph3D instance;
* required for access to settings
* @param {Boolean} withBars True if initializing for bar graph
*/
DataGroup.prototype._initializeRange = function(data, column, graph3d, withBars) {
var NUMSTEPS = 5;
var settings = this._collectRangeSettings(column, graph3d);
var range = this.getColumnRange(data, column);
if (withBars && column != 'z') { // Safeguard for 'z'; it doesn't have a bar width
range.expand(settings.barWidth / 2);
}
this._setRangeDefaults(range, settings.min, settings.max);
this[settings.range_label] = range;
this[settings.step_label ] = (settings.step !== undefined) ? settings.step : range.range()/NUMSTEPS;
}
/**
* Creates a list with all the different values in the data for the given column.
*
* If no data passed, use the internal data of this instance.
*
* @param {'x'|'y'|'z'} column The data column to process
* @param {DataSet|DataView|undefined} data The data containing the items for the Graph
*
* @returns {Array} All distinct values in the given column data, sorted ascending.
*/
DataGroup.prototype.getDistinctValues = function(column, data) {
if (data === undefined) {
data = this.dataTable;
}
var values = [];
for (var i = 0; i < data.length; i++) {
var value = data[i][column] || 0;
if (values.indexOf(value) === -1) {
values.push(value);
}
}
return values.sort(function(a,b) { return a - b; });
};
/**
* Determine the smallest difference between the values for given
* column in the passed data set.
*
* @param {DataSet|DataView|undefined} data The data containing the items for the Graph
* @param {'x'|'y'|'z'} column The data column to process
*
* @returns {Number|null} Smallest difference value or
* null, if it can't be determined.
*/
DataGroup.prototype.getSmallestDifference = function(data, column) {
var values = this.getDistinctValues(data, column);
// Get all the distinct diffs
// Array values is assumed to be sorted here
var smallest_diff = null;
for (var i = 1; i < values.length; i++) {
var diff = values[i] - values[i - 1];
if (smallest_diff == null || smallest_diff > diff ) {
smallest_diff = diff;
}
}
return smallest_diff;
}
/**
* Get the absolute min/max values for the passed data column.
*
* @param {DataSet|DataView|undefined} data The data containing the items for the Graph
* @param {'x'|'y'|'z'} column The data column to process
*
* @returns {Range} A Range instance with min/max members properly set.
*/
DataGroup.prototype.getColumnRange = function(data, column) {
var range = new Range();
// Adjust the range so that it covers all values in the passed data elements.
for (var i = 0; i < data.length; i++) {
var item = data[i][column];
range.adjust(item);
}
return range;
};
/**
* Determines the number of rows in the current data.
*
* @returns {Number}
*/
DataGroup.prototype.getNumberOfRows = function() {
return this.dataTable.length;
}
/**
* Set default values for range
*
* The default values override the range values, if defined.
*
* Because it's possible that only defaultMin or defaultMax is set, it's better
* to pass in a range already set with the min/max set from the data. Otherwise,
* it's quite hard to process the min/max properly.
*/
DataGroup.prototype._setRangeDefaults = function (range, defaultMin, defaultMax) {
if (defaultMin !== undefined) {
range.min = defaultMin;
}
if (defaultMax !== undefined) {
range.max = defaultMax;
}
// This is the original way that the default min/max values were adjusted.
// TODO: Perhaps it's better if an error is thrown if the values do not agree.
// But this will change the behaviour.
if (range.max <= range.min) range.max = range.min + 1;
};
DataGroup.prototype.getDataTable = function() {
return this.dataTable;
};
DataGroup.prototype.getDataSet = function() {
return this.dataSet;
};
/**
* Reload the data
*/
DataGroup.prototype.reload = function() {
if (this.dataTable) {
this.setData(this.dataTable);
}
};
module.exports = DataGroup;

+ 4
- 9
lib/graph3d/Filter.js View File

@ -3,12 +3,12 @@ var DataView = require('../DataView');
/**
* @class Filter
*
* @param {DataSet} data The google data table
* @param {DataGroup} dataGroup the data group
* @param {Number} column The index of the column to be filtered
* @param {Graph} graph The graph
*/
function Filter (data, column, graph) {
this.data = data;
function Filter (dataGroup, column, graph) {
this.data = dataGroup.getDataSet();
this.column = column;
this.graph = graph; // the parent graph
@ -16,12 +16,7 @@ function Filter (data, column, graph) {
this.value = undefined;
// read all distinct values and select the first one
this.values = graph.getDistinctValues(data.get(), this.column);
// sort both numeric and string values correctly
this.values.sort(function (a, b) {
return a > b ? 1 : a < b ? -1 : 0;
});
this.values = dataGroup.getDistinctValues(this.column);
if (this.values.length > 0) {
this.selectValue(0);

+ 91
- 277
lib/graph3d/Graph3d.js View File

@ -1,4 +1,5 @@
var Emitter = require('emitter-component'); var DataSet = require('../DataSet');
var Emitter = require('emitter-component');
var DataSet = require('../DataSet');
var DataView = require('../DataView');
var util = require('../util');
var Point3d = require('./Point3d');
@ -9,6 +10,7 @@ var Slider = require('./Slider');
var StepNumber = require('./StepNumber');
var Range = require('./Range');
var Settings = require('./Settings');
var DataGroup = require('./DataGroup');
/// enumerate the available styles
@ -148,7 +150,7 @@ function Graph3d(container, data, options) {
// create variables and set default values
this.containerElement = container;
this.dataTable = null; // The original data table
this.dataGroup = new DataGroup();
this.dataPoints = null; // The table with point objects
// create a frame and canvas
@ -303,11 +305,7 @@ Graph3d.prototype._convertTranslationToScreen = function(translation) {
/**
* Calculate the translations and screen positions of all points
*/
Graph3d.prototype._calcTranslations = function(points, sort) {
if (sort === undefined) {
sort = true;
}
Graph3d.prototype._calcTranslations = function(points) {
for (var i = 0; i < points.length; i++) {
var point = points[i];
point.trans = this._convertPointToTranslation(point.point);
@ -318,10 +316,6 @@ Graph3d.prototype._calcTranslations = function(points, sort) {
point.dist = this.showPerspective ? transBottom.length() : -transBottom.z;
}
if (!sort) {
return;
}
// sort the points on depth of their (x,y) position (not on z)
var sortDepth = function (a, b) {
return b.dist - a.dist;
@ -330,78 +324,6 @@ Graph3d.prototype._calcTranslations = function(points, sort) {
};
Graph3d.prototype.getNumberOfRows = function(data) {
return data.length;
}
Graph3d.prototype.getNumberOfColumns = function(data) {
var counter = 0;
for (var column in data[0]) {
if (data[0].hasOwnProperty(column)) {
counter++;
}
}
return counter;
}
Graph3d.prototype.getDistinctValues = function(data, column) {
var distinctValues = [];
for (var i = 0; i < data.length; i++) {
if (distinctValues.indexOf(data[i][column]) == -1) {
distinctValues.push(data[i][column]);
}
}
return distinctValues.sort(function(a,b) { return a - b; });
}
/**
* Determine the smallest difference between the values for given
* column in the passed data set.
*
* @returns {Number|null} Smallest difference value or
* null, if it can't be determined.
*/
Graph3d.prototype.getSmallestDifference = function(data, column) {
var values = this.getDistinctValues(data, column);
var diffs = [];
// Get all the distinct diffs
// Array values is assumed to be sorted here
var smallest_diff = null;
for (var i = 1; i < values.length; i++) {
var diff = values[i] - values[i - 1];
if (smallest_diff == null || smallest_diff > diff ) {
smallest_diff = diff;
}
}
return smallest_diff;
}
/**
* Get the absolute min/max values for the passed data column.
*
* @returns {Range} A Range instance with min/max members properly set.
*/
Graph3d.prototype.getColumnRange = function(data,column) {
var range = new Range();
// Adjust the range so that it covers all values in the passed data elements.
for (var i = 0; i < data.length; i++) {
var item = data[i][column];
range.adjust(item);
}
return range;
};
/**
* Check if the state is consistent for the use of the value field.
*
@ -418,6 +340,7 @@ Graph3d.prototype._checkValueField = function (data) {
return; // No need to check further
}
// Following field must be present for the current graph style
if (this.colValue === undefined) {
throw new Error('Expected data to have '
@ -437,150 +360,76 @@ Graph3d.prototype._checkValueField = function (data) {
};
/**
* Set default values for range
*
* The default values override the range values, if defined.
*
* Because it's possible that only defaultMin or defaultMax is set, it's better
* to pass in a range already set with the min/max set from the data. Otherwise,
* it's quite hard to process the min/max properly.
*/
Graph3d.prototype._setRangeDefaults = function (range, defaultMin, defaultMax) {
if (defaultMin !== undefined) {
range.min = defaultMin;
}
if (defaultMax !== undefined) {
range.max = defaultMax;
}
// This is the original way that the default min/max values were adjusted.
// TODO: Perhaps it's better if an error is thrown if the values do not agree.
// But this will change the behaviour.
if (range.max <= range.min) range.max = range.min + 1;
};
/**
* Initialize the data from the data table. Calculate minimum and maximum values
* and column index values
* @param {Array | DataSet | DataView} rawData The data containing the items for
* the Graph.
* @param {Number} style Style Number
*/
Graph3d.prototype._dataInitialize = function (rawData, style) {
var me = this;
// unsubscribe from the dataTable
if (this.dataSet) {
this.dataSet.off('*', this._onChange);
}
Graph3d.prototype._initializeData = function(rawData, style) {
this.dataGroup.initializeData(this, rawData, style);
if (rawData === undefined)
return;
// Transfer min/max values to the Graph3d instance.
// TODO: later on, all min/maxes of all datagroups will be combined here
this.xRange = this.dataGroup.xRange;
this.yRange = this.dataGroup.yRange;
this.zRange = this.dataGroup.zRange;
this.valueRange = this.dataGroup.valueRange;
if (Array.isArray(rawData)) {
rawData = new DataSet(rawData);
}
// Values currently needed but which need to be sorted out for
// the multiple graph case.
this.xStep = this.dataGroup.xStep;
this.yStep = this.dataGroup.yStep;
this.zStep = this.dataGroup.zStep;
this.xBarWidth = this.dataGroup.xBarWidth;
this.yBarWidth = this.dataGroup.yBarWidth;
this.colX = this.dataGroup.colX;
this.colY = this.dataGroup.colY;
this.colZ = this.dataGroup.colZ;
this.colValue = this.dataGroup.colValue;
var data;
if (rawData instanceof DataSet || rawData instanceof DataView) {
data = rawData.get();
}
else {
throw new Error('Array, DataSet, or DataView expected');
}
// Check if a filter column is provided
var data = this.dataGroup.getDataTable();
if (data.length == 0)
return;
this.dataSet = rawData;
this.dataTable = data;
// subscribe to changes in the dataset
this._onChange = function () {
me.setData(me.dataSet);
};
this.dataSet.on('*', this._onChange);
// determine the location of x,y,z,value,filter columns
this.colX = 'x';
this.colY = 'y';
this.colZ = 'z';
var withBars = this.style == Graph3d.STYLE.BAR ||
this.style == Graph3d.STYLE.BARCOLOR ||
this.style == Graph3d.STYLE.BARSIZE;
// determine barWidth from data
if (withBars) {
if (this.defaultXBarWidth !== undefined) {
this.xBarWidth = this.defaultXBarWidth;
}
else {
this.xBarWidth = this.getSmallestDifference(data, this.colX) || 1;
}
if (this.defaultYBarWidth !== undefined) {
this.yBarWidth = this.defaultYBarWidth;
}
else {
this.yBarWidth = this.getSmallestDifference(data, this.colY) || 1;
}
}
// calculate minimums and maximums
var NUMSTEPS = 5;
var xRange = this.getColumnRange(data, this.colX);
if (withBars) {
xRange.expand(this.xBarWidth / 2);
}
this._setRangeDefaults(xRange, this.defaultXMin, this.defaultXMax);
this.xRange = xRange;
this.xStep = (this.defaultXStep !== undefined) ? this.defaultXStep : xRange.range()/NUMSTEPS;
var yRange = this.getColumnRange(data, this.colY);
if (withBars) {
yRange.expand(this.yBarWidth / 2);
}
this._setRangeDefaults(yRange, this.defaultYMin, this.defaultYMax);
this.yRange = yRange;
this.yStep = (this.defaultYStep !== undefined) ? this.defaultYStep : yRange.range()/NUMSTEPS;
var zRange = this.getColumnRange(data, this.colZ);
this._setRangeDefaults(zRange, this.defaultZMin, this.defaultZMax);
this.zRange = zRange;
this.zStep = (this.defaultZStep !== undefined) ? this.defaultZStep : zRange.range()/NUMSTEPS;
if (data[0].hasOwnProperty('style')) {
this.colValue = 'style';
var valueRange = this.getColumnRange(data,this.colValue);
this._setRangeDefaults(valueRange, this.defaultValueMin, this.defaultValueMax);
this.valueRange = valueRange;
}
// check if a filter column is provided
// Needs to be started after zRange is defined
if (data[0].hasOwnProperty('filter')) {
// Only set this field if it's actually present
this.colFilter = 'filter';
var me = this;
if (this.dataFilter === undefined) {
this.dataFilter = new Filter(rawData, this.colFilter, this);
this.dataFilter = new Filter(this.dataGroup, this.colFilter, this);
this.dataFilter.setOnLoadCallback(function() {me.redraw();});
}
}
// set the scale dependent on the ranges.
this._setScale();
};
/**
* Return all data values as a list of Point3d objects
*/
Graph3d.prototype.getDataPoints = function(data) {
var dataPoints = [];
for (var i = 0; i < data.length; i++) {
var point = new Point3d();
point.x = data[i][this.colX] || 0;
point.y = data[i][this.colY] || 0;
point.z = data[i][this.colZ] || 0;
point.data = data[i];
if (this.colValue !== undefined) {
point.value = data[i][this.colValue] || 0;
}
var obj = {};
obj.point = point;
obj.bottom = new Point3d(point.x, point.y, this.zRange.min);
obj.trans = undefined;
obj.screen = undefined;
dataPoints.push(obj);
}
return dataPoints;
};
/**
* Filter the data based on the current filter
@ -598,60 +447,29 @@ Graph3d.prototype._getDataPoints = function (data) {
if (this.style === Graph3d.STYLE.GRID ||
this.style === Graph3d.STYLE.SURFACE) {
// copy all values from the google data table to a matrix
// copy all values from the data table to a matrix
// the provided values are supposed to form a grid of (x,y) positions
// create two lists with all present x and y values
var dataX = [];
var dataY = [];
for (i = 0; i < this.getNumberOfRows(data); i++) {
x = data[i][this.colX] || 0;
y = data[i][this.colY] || 0;
if (dataX.indexOf(x) === -1) {
dataX.push(x);
}
if (dataY.indexOf(y) === -1) {
dataY.push(y);
}
}
var dataX = this.dataGroup.getDistinctValues(this.colX, data);
var dataY = this.dataGroup.getDistinctValues(this.colY, data);
var sortNumber = function (a, b) {
return a - b;
};
dataX.sort(sortNumber);
dataY.sort(sortNumber);
dataPoints = this.getDataPoints(data);
// create a grid, a 2d matrix, with all values.
var dataMatrix = []; // temporary data matrix
for (i = 0; i < data.length; i++) {
x = data[i][this.colX] || 0;
y = data[i][this.colY] || 0;
z = data[i][this.colZ] || 0;
for (i = 0; i < dataPoints.length; i++) {
obj = dataPoints[i];
// TODO: implement Array().indexOf() for Internet Explorer
var xIndex = dataX.indexOf(x);
var yIndex = dataY.indexOf(y);
var xIndex = dataX.indexOf(obj.point.x);
var yIndex = dataY.indexOf(obj.point.y);
if (dataMatrix[xIndex] === undefined) {
dataMatrix[xIndex] = [];
}
var point3d = new Point3d();
point3d.x = x;
point3d.y = y;
point3d.z = z;
point3d.data = data[i];
obj = {};
obj.point = point3d;
obj.trans = undefined;
obj.screen = undefined;
obj.bottom = new Point3d(x, y, this.zRange.min);
dataMatrix[xIndex][yIndex] = obj;
dataPoints.push(obj);
}
// fill in the pointers to the neighbors.
@ -670,39 +488,22 @@ Graph3d.prototype._getDataPoints = function (data) {
}
else { // 'dot', 'dot-line', etc.
this._checkValueField(data);
dataPoints = this.getDataPoints(data);
// copy all values from the google data table to a list with Point3d objects
for (i = 0; i < data.length; i++) {
point = new Point3d();
point.x = data[i][this.colX] || 0;
point.y = data[i][this.colY] || 0;
point.z = data[i][this.colZ] || 0;
point.data = data[i];
if (this.colValue !== undefined) {
point.value = data[i][this.colValue] || 0;
}
obj = {};
obj.point = point;
obj.bottom = new Point3d(point.x, point.y, this.zRange.min);
obj.trans = undefined;
obj.screen = undefined;
if (this.style === Graph3d.STYLE.LINE) {
if (this.style === Graph3d.STYLE.LINE) {
// Add next member points for line drawing
for (i = 0; i < dataPoints.length; i++) {
if (i > 0) {
// Add next point for line drawing
dataPoints[i - 1].pointNext = obj;
dataPoints[i - 1].pointNext = dataPoints[i];;
}
}
dataPoints.push(obj);
}
}
return dataPoints;
};
/**
* Create the main frame for the Graph3d.
*
@ -854,7 +655,7 @@ Graph3d.prototype.getCameraPosition = function() {
*/
Graph3d.prototype._readData = function(data) {
// read the data
this._dataInitialize(data, this.style);
this._initializeData(data, this.style);
if (this.dataFilter) {
@ -863,7 +664,7 @@ Graph3d.prototype._readData = function(data) {
}
else {
// no filtering. load all data
this.dataPoints = this._getDataPoints(this.dataTable);
this.dataPoints = this._getDataPoints(this.dataGroup.getDataTable());
}
// draw the filter
@ -901,9 +702,7 @@ Graph3d.prototype.setOptions = function (options) {
this._setSize(this.width, this.height);
// re-load the data
if (this.dataTable) {
this.setData(this.dataTable);
}
this.dataGroup.reload();
// start animation when option is true
if (this.animationAutoStart && this.dataFilter) {
@ -1138,6 +937,7 @@ Graph3d.prototype._redrawLegend = function() {
ctx.fillText(label, right, bottom + this.margin);
};
/**
* Redraw the filter
*/
@ -2335,6 +2135,20 @@ Graph3d.prototype._dataPointFromXY = function (x, y) {
return closestDataPoint;
};
/**
* Determine if the given style has bars
*
* @param {number} style the style to check
* @returns {boolean} true if bar style, false otherwise
*/
Graph3d.prototype.hasBars = function(style) {
return style == Graph3d.STYLE.BAR ||
style == Graph3d.STYLE.BARCOLOR ||
style == Graph3d.STYLE.BARSIZE;
};
/**
* Display a tooltip for given data point
* @param {Object} dataPoint

+ 4
- 0
lib/network/CachedImage.js View File

@ -66,6 +66,10 @@ class CachedImage {
* This methods takes the resizing out of the drawing loop, in order to
* reduce performance overhead.
*
* TODO: The code assumes that a 2D context can always be gotten. This is
* not necessarily true! OTOH, if not true then usage of this class
* is senseless.
*
* @private
*/
_fillMipMap() {

+ 15
- 1
lib/network/Network.js View File

@ -55,13 +55,27 @@ function Network(container, data, options) {
};
util.extend(this.options, this.defaultOptions);
// containers for nodes and edges
/**
* Containers for nodes and edges.
*
* 'edges' and 'nodes' contain the full definitions of all the network elements.
* 'nodeIndices' and 'edgeIndices' contain the id's of the active elements.
*
* The distinction is important, because a defined node need not be active, i.e.
* visible on the canvas. This happens in particular when clusters are defined, in
* that case there will be nodes and edges not displayed.
* The bottom line is that all code with actions related to visibility, *must* use
* 'nodeIndices' and 'edgeIndices', not 'nodes' and 'edges' directly.
*/
this.body = {
container: container,
// See comment above for following fields
nodes: {},
nodeIndices: [],
edges: {},
edgeIndices: [],
emitter: {
on: this.on.bind(this),
off: this.off.bind(this),

+ 30
- 2
lib/network/dotparser.js View File

@ -10,6 +10,29 @@
* @return {Object} graph An object containing two parameters:
* {Object[]} nodes
* {Object[]} edges
*
* -------------------------------------------
* TODO
* ====
*
* For label handling, this is an incomplete implementation. From docs (quote #3015):
*
* > the escape sequences "\n", "\l" and "\r" divide the label into lines, centered,
* > left-justified, and right-justified, respectively.
*
* Source: http://www.graphviz.org/content/attrs#kescString
*
* > As another aid for readability, dot allows double-quoted strings to span multiple physical
* > lines using the standard C convention of a backslash immediately preceding a newline
* > character
* > In addition, double-quoted strings can be concatenated using a '+' operator.
* > As HTML strings can contain newline characters, which are used solely for formatting,
* > the language does not allow escaped newlines or concatenation operators to be used
* > within them.
*
* - Currently, only '\\n' is handled
* - Note that text explicitly says 'labels'; the dot parser currently handles escape
* sequences in **all** strings.
*/
function parseDOT (data) {
dot = data;
@ -358,9 +381,14 @@ function getToken() {
if (c === '"') {
next();
while (c != '' && (c != '"' || (c === '"' && nextPreview() === '"'))) {
token += c;
if (c === '"') { // skip the escape character
if (c === '"') { // skip the escape character
token += c;
next();
} else if (c === '\\' && nextPreview() === 'n') { // Honor a newline escape sequence
token += '\n';
next();
} else {
token += c;
}
next();
}

+ 51
- 7
lib/network/modules/Canvas.js View File

@ -188,9 +188,8 @@ class Canvas {
this.frame.canvas.appendChild(noCanvas);
}
else {
let ctx = this.frame.canvas.getContext("2d");
this._setPixelRatio(ctx);
this.frame.canvas.getContext("2d").setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0);
this._setPixelRatio();
this.setTransform();
}
// add the frame to the container element
@ -257,9 +256,19 @@ class Canvas {
let oldHeight = this.frame.canvas.height;
// update the pixel ratio
let ctx = this.frame.canvas.getContext("2d");
//
// NOTE: Comment in following is rather inconsistent; this is the ONLY place in the code
// where it is assumed that the pixel ratio could change at runtime.
// The only way I can think of this happening is a rotating screen or tablet; but then
// there should be a mechanism for reloading the data (TODO: check if this is present).
//
// If the assumption is true (i.e. pixel ratio can change at runtime), then *all* usage
// of pixel ratio must be overhauled for this.
//
// For the time being, I will humor the assumption here, and in the rest of the code assume it is
// constant.
let previousRatio = this.pixelRatio; // we cache this because the camera state storage needs the old value
this._setPixelRatio(ctx);
this._setPixelRatio();
if (width != this.options.width || height != this.options.height || this.frame.style.width != width || this.frame.style.height != height) {
this._getCameraState(previousRatio);
@ -324,11 +333,23 @@ class Canvas {
};
getContext() {
return this.frame.canvas.getContext("2d");
}
/**
* Determine the pixel ratio for various browsers.
*
* @private
*/
_setPixelRatio(ctx) {
this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio ||
_determinePixelRatio() {
let ctx = this.getContext();
if (ctx === undefined) {
throw "Could not get canvax context";
}
return (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||