From 7e6810955d34ff127f58ab47d58ad050cd9add94 Mon Sep 17 00:00:00 2001 From: josdejong Date: Fri, 3 Jan 2014 13:22:44 +0100 Subject: [PATCH] Switched to 2-space indentation --- HISTORY.md | 2 + Jakefile.js | 240 +- README.md | 54 +- bower.json | 42 +- docs/css/prettify.css | 102 +- docs/css/style.css | 78 +- docs/dataset.html | 1102 +- docs/dataview.html | 272 +- docs/graph.html | 1628 +- docs/index.html | 252 +- docs/timeline.html | 918 +- examples/graph/01_basic_usage.html | 68 +- examples/graph/02_random_nodes.html | 168 +- examples/graph/03_images.html | 144 +- examples/graph/04_shapes.html | 114 +- examples/graph/05_social_network.html | 116 +- examples/graph/06_groups.html | 282 +- examples/graph/07_selections.html | 88 +- examples/graph/08_mobile_friendly.html | 174 +- examples/graph/09_sizing.html | 126 +- examples/graph/10_multiline_text.html | 72 +- examples/graph/11_custom_style.html | 228 +- examples/graph/12_scalable_images.html | 132 +- examples/graph/13_dashed_lines.html | 104 +- examples/graph/14_dot_language.html | 20 +- .../graph/15_dot_language_playground.html | 344 +- examples/graph/16_dynamic_data.html | 448 +- examples/graph/17_network_info.html | 280 +- examples/graph/graphviz/graphviz_gallery.html | 120 +- examples/graph/index.html | 42 +- package.json | 64 +- src/DataSet.js | 1180 +- src/DataView.js | 335 +- src/EventBus.js | 86 +- src/events.js | 196 +- src/graph/Edge.js | 766 +- src/graph/Graph.js | 2348 +-- src/graph/Groups.js | 74 +- src/graph/Images.js | 34 +- src/graph/Node.js | 750 +- src/graph/Popup.js | 124 +- src/graph/dotparser.js | 1502 +- src/graph/shapes.js | 438 +- src/module/exports.js | 76 +- src/shim.js | 346 +- src/timeline/Controller.js | 220 +- src/timeline/Range.js | 564 +- src/timeline/Stack.js | 214 +- src/timeline/TimeStep.js | 566 +- src/timeline/Timeline.js | 544 +- src/timeline/component/Component.js | 116 +- src/timeline/component/ContentPanel.js | 104 +- src/timeline/component/CurrentTime.js | 114 +- src/timeline/component/CustomTime.js | 282 +- src/timeline/component/Group.js | 122 +- src/timeline/component/GroupSet.js | 668 +- src/timeline/component/ItemSet.js | 676 +- src/timeline/component/Panel.js | 104 +- src/timeline/component/RootPanel.js | 252 +- src/timeline/component/TimeAxis.js | 716 +- src/timeline/component/css/currenttime.css | 6 +- src/timeline/component/css/customtime.css | 8 +- src/timeline/component/css/groupset.css | 68 +- src/timeline/component/css/item.css | 88 +- src/timeline/component/css/itemset.css | 10 +- src/timeline/component/css/panel.css | 14 +- src/timeline/component/css/timeaxis.css | 42 +- src/timeline/component/item/Item.js | 40 +- src/timeline/component/item/ItemBox.js | 468 +- src/timeline/component/item/ItemPoint.js | 334 +- src/timeline/component/item/ItemRange.js | 338 +- .../component/item/ItemRangeOverflow.js | 110 +- src/util.js | 820 +- test/dataset.html | 120 +- test/dataset.js | 122 +- test/dataview.js | 32 +- test/dotparser.js | 328 +- test/eventbus.js | 26 +- test/timeline.html | 132 +- test/timeline_groups.html | 118 +- test/timestep.html | 44 +- tools/watch.js | 14 +- vis.js | 16023 ++++++++-------- vis.min.js | 13 +- 84 files changed, 20071 insertions(+), 20588 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index c647a7a6..173f4445 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,9 +1,11 @@ vis.js history http://visjs.org + ## , version 0.3.0 - Implemented options `showCurrentTime` and `showCustomTime`. Thanks fi0dor. +- Implemented touch support for Timeline. - Fixed broken Timeline options `min` and `max`. - Fixed not being able to load vis.js in node.js. diff --git a/Jakefile.js b/Jakefile.js index c2961da0..d5599feb 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -18,7 +18,7 @@ var VIS_MIN = './vis.min.js'; */ desc('Execute all tasks: build all libraries'); task('default', ['build', 'minify', 'test'], function () { - console.log('done'); + console.log('done'); }); /** @@ -26,97 +26,97 @@ task('default', ['build', 'minify', 'test'], function () { */ desc('Build the visualization library vis.js'); task('build', {async: true}, function () { - // concatenate and stringify the css files - var result = concat({ - src: [ - './src/timeline/component/css/timeline.css', - './src/timeline/component/css/panel.css', - './src/timeline/component/css/groupset.css', - './src/timeline/component/css/itemset.css', - './src/timeline/component/css/item.css', - './src/timeline/component/css/timeaxis.css', - './src/timeline/component/css/currenttime.css', - './src/timeline/component/css/customtime.css' - ], - header: '/* vis.js stylesheet */', - separator: '\n' - }); - var cssText = JSON.stringify(result.code); - - // concatenate the script files - concat({ - dest: VIS_TMP, - src: [ - './src/module/imports.js', - - './src/shim.js', - './src/util.js', - './src/events.js', - './src/EventBus.js', - './src/DataSet.js', - './src/DataView.js', - - './src/timeline/TimeStep.js', - './src/timeline/Stack.js', - './src/timeline/Range.js', - './src/timeline/Controller.js', - './src/timeline/component/Component.js', - './src/timeline/component/Panel.js', - './src/timeline/component/RootPanel.js', - './src/timeline/component/TimeAxis.js', - './src/timeline/component/CurrentTime.js', - './src/timeline/component/CustomTime.js', - './src/timeline/component/ItemSet.js', - './src/timeline/component/item/*.js', - './src/timeline/component/Group.js', - './src/timeline/component/GroupSet.js', - './src/timeline/Timeline.js', - - './src/graph/dotparser.js', - './src/graph/shapes.js', - './src/graph/Node.js', - './src/graph/Edge.js', - './src/graph/Popup.js', - './src/graph/Groups.js', - './src/graph/Images.js', - './src/graph/Graph.js', - - './src/module/exports.js' - ], - - separator: '\n', - - // Note: we insert the css as a string in the javascript code here - // the css will be injected on load of the javascript library - footer: '// inject css\n' + - 'util.loadCss(' + cssText + ');\n' - }); - - // bundle the concatenated script and dependencies into one file - var b = browserify(); - b.add(VIS_TMP); - b.bundle({ - standalone: 'vis' - }, function (err, code) { - if(err) { - throw err; - } - - // add header and footer - var lib = read('./src/module/header.js') + code; - - // write bundled file - write(VIS, lib); - console.log('created ' + VIS); - - // remove temporary file - fs.unlinkSync(VIS_TMP); - - // update version number and stuff in the javascript files - replacePlaceholders(VIS); - - complete(); - }); + // concatenate and stringify the css files + var result = concat({ + src: [ + './src/timeline/component/css/timeline.css', + './src/timeline/component/css/panel.css', + './src/timeline/component/css/groupset.css', + './src/timeline/component/css/itemset.css', + './src/timeline/component/css/item.css', + './src/timeline/component/css/timeaxis.css', + './src/timeline/component/css/currenttime.css', + './src/timeline/component/css/customtime.css' + ], + header: '/* vis.js stylesheet */', + separator: '\n' + }); + var cssText = JSON.stringify(result.code); + + // concatenate the script files + concat({ + dest: VIS_TMP, + src: [ + './src/module/imports.js', + + './src/shim.js', + './src/util.js', + './src/events.js', + './src/EventBus.js', + './src/DataSet.js', + './src/DataView.js', + + './src/timeline/TimeStep.js', + './src/timeline/Stack.js', + './src/timeline/Range.js', + './src/timeline/Controller.js', + './src/timeline/component/Component.js', + './src/timeline/component/Panel.js', + './src/timeline/component/RootPanel.js', + './src/timeline/component/TimeAxis.js', + './src/timeline/component/CurrentTime.js', + './src/timeline/component/CustomTime.js', + './src/timeline/component/ItemSet.js', + './src/timeline/component/item/*.js', + './src/timeline/component/Group.js', + './src/timeline/component/GroupSet.js', + './src/timeline/Timeline.js', + + './src/graph/dotparser.js', + './src/graph/shapes.js', + './src/graph/Node.js', + './src/graph/Edge.js', + './src/graph/Popup.js', + './src/graph/Groups.js', + './src/graph/Images.js', + './src/graph/Graph.js', + + './src/module/exports.js' + ], + + separator: '\n', + + // Note: we insert the css as a string in the javascript code here + // the css will be injected on load of the javascript library + footer: '// inject css\n' + + 'util.loadCss(' + cssText + ');\n' + }); + + // bundle the concatenated script and dependencies into one file + var b = browserify(); + b.add(VIS_TMP); + b.bundle({ + standalone: 'vis' + }, function (err, code) { + if(err) { + throw err; + } + + // add header and footer + var lib = read('./src/module/header.js') + code; + + // write bundled file + write(VIS, lib); + console.log('created ' + VIS); + + // remove temporary file + fs.unlinkSync(VIS_TMP); + + // update version number and stuff in the javascript files + replacePlaceholders(VIS); + + complete(); + }); }); /** @@ -124,17 +124,17 @@ task('build', {async: true}, function () { */ desc('Minify the visualization library vis.js'); task('minify', function () { - // minify javascript - minify({ - src: VIS, - dest: VIS_MIN, - header: read('./src/module/header.js') - }); + // minify javascript + minify({ + src: VIS, + dest: VIS_MIN, + header: read('./src/module/header.js') + }); - // update version number and stuff in the javascript files - replacePlaceholders(VIS_MIN); + // update version number and stuff in the javascript files + replacePlaceholders(VIS_MIN); - console.log('created ' + VIS_MIN); + console.log('created ' + VIS_MIN); }); /** @@ -142,18 +142,18 @@ task('minify', function () { */ desc('Test the library'); task('test', ['build'], function () { - // TODO: use a testing suite for testing: nodeunit, mocha, tap, ... - var filelist = new jake.FileList(); - filelist.include([ - './test/**/*.js' - ]); - - var files = filelist.toArray(); - files.forEach(function (file) { - require('./' + file); - }); - - console.log('Executed ' + files.length + ' test files successfully'); + // TODO: use a testing suite for testing: nodeunit, mocha, tap, ... + var filelist = new jake.FileList(); + filelist.include([ + './test/**/*.js' + ]); + + var files = filelist.toArray(); + files.forEach(function (file) { + require('./' + file); + }); + + console.log('Executed ' + files.length + ' test files successfully'); }); /** @@ -161,11 +161,11 @@ task('test', ['build'], function () { * @param {String} filename */ var replacePlaceholders = function (filename) { - replace({ - replacements: [ - {pattern: '@@date', replacement: today()}, - {pattern: '@@version', replacement: version()} - ], - src: filename - }); + replace({ + replacements: [ + {pattern: '@@date', replacement: today()}, + {pattern: '@@version', replacement: version()} + ], + src: filename + }); }; diff --git a/README.md b/README.md index ea589e3c..a97439b5 100644 --- a/README.md +++ b/README.md @@ -40,12 +40,12 @@ To use a component, include the javascript file of vis in your web page: - + - + ``` @@ -54,12 +54,12 @@ or load vis.js using require.js: ```js require.config({ - paths: { - vis: 'path/to/vis', - } + paths: { + vis: 'path/to/vis', + } }); require(['vis'], function (math) { - // ... load a visualization + // ... load a visualization }); ``` @@ -85,30 +85,30 @@ of the project. - Timeline basic demo - - - + Timeline basic demo + + +
diff --git a/bower.json b/bower.json index 29dec11a..742c61d9 100644 --- a/bower.json +++ b/bower.json @@ -1,23 +1,23 @@ { - "name": "vis", - "version": "0.3.0-SNAPSHOT", - "description": "A dynamic, browser-based visualization library.", - "homepage": "http://visjs.org/", - "repository": { - "type": "git", - "url": "git://github.com/almende/vis.git" - }, - "ignore": [ - "node_modules", - "src", - "test", - "tools", - ".idea", - "Jakefile.js", - "package.json", - ".npmignore", - ".gitignore" - ], - "dependencies": {}, - "devDependencies": {} + "name": "vis", + "version": "0.3.0-SNAPSHOT", + "description": "A dynamic, browser-based visualization library.", + "homepage": "http://visjs.org/", + "repository": { + "type": "git", + "url": "git://github.com/almende/vis.git" + }, + "ignore": [ + "node_modules", + "src", + "test", + "tools", + ".idea", + "Jakefile.js", + "package.json", + ".npmignore", + ".gitignore" + ], + "dependencies": {}, + "devDependencies": {} } diff --git a/docs/css/prettify.css b/docs/css/prettify.css index b4ec4ca0..3c7acd2e 100644 --- a/docs/css/prettify.css +++ b/docs/css/prettify.css @@ -1,87 +1,87 @@ .com { - color: gray; + color: gray; } .lit { - color: red; + color: red; } .pun { - color: gray; + color: gray; } .pln { - color: #333333; + color: #333333; } pre.prettyprint { - border: 1px solid lightgray; - background-color: #fcfcfc; - padding: 5px; + border: 1px solid lightgray; + background-color: #fcfcfc; + padding: 5px; - font-size: 10pt; - line-height: 1.5em; - font-family: monospace; + font-size: 10pt; + line-height: 1.5em; + font-family: monospace; } ol.linenums { - margin-top:0; - margin-bottom:0; + margin-top:0; + margin-bottom:0; } li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8 { - list-style:none; + list-style:none; } li.L1,li.L3,li.L5,li.L7,li.L9 { - background:#eee; + background:#eee; } .str,.atv { - color: green; + color: green; } .kwd,.tag { - color:#2B7CE9; + color:#2B7CE9; } .typ,.atn,.dec { - color: darkorange; + color: darkorange; } @media print { - .com { - color:#600; - font-style:italic; - } - - .typ { - color:#404; - font-weight:700; - } - - .lit { - color:#044; - } - - .pun { - color:#440; - } - - .pln { - color:#000; - } - - .atn { - color:#404; - } - - .str,.atv { - color:#060; - } - - .kwd,.tag { - color:#006; - font-weight:700; - } + .com { + color:#600; + font-style:italic; + } + + .typ { + color:#404; + font-weight:700; + } + + .lit { + color:#044; + } + + .pun { + color:#440; + } + + .pln { + color:#000; + } + + .atn { + color:#404; + } + + .str,.atv { + color:#060; + } + + .kwd,.tag { + color:#006; + font-weight:700; + } } \ No newline at end of file diff --git a/docs/css/style.css b/docs/css/style.css index 14ba2183..12b5ac45 100644 --- a/docs/css/style.css +++ b/docs/css/style.css @@ -1,77 +1,77 @@ html, body { - width: 100%; - height: 100%; - padding: 0; - margin: 0; + width: 100%; + height: 100%; + padding: 0; + margin: 0; } body, td, th { - font-family: arial, sans-serif; - font-size: 11pt; - color: #4D4D4D; - line-height: 1.7em; + font-family: arial, sans-serif; + font-size: 11pt; + color: #4D4D4D; + line-height: 1.7em; } #container { - position: relative; - margin: 0 auto; - padding: 10px 10px 50px 10px; - width: 700px; - max-width: 100%; - box-sizing: border-box; + position: relative; + margin: 0 auto; + padding: 10px 10px 50px 10px; + width: 700px; + max-width: 100%; + box-sizing: border-box; } h1 { - font-size: 180%; - font-weight: bold; - padding: 0; - margin: 1em 0 1em 0; + font-size: 180%; + font-weight: bold; + padding: 0; + margin: 1em 0 1em 0; } h2 { - padding-top: 20px; - padding-bottom: 10px; - border-bottom: 1px solid #a0c0f0; - color: #2B7CE9; + padding-top: 20px; + padding-bottom: 10px; + border-bottom: 1px solid #a0c0f0; + color: #2B7CE9; } h3 { - font-size: 140%; + font-size: 140%; } a > img { - border: none; + border: none; } a { - color: #2B7CE9; - text-decoration: none; + color: #2B7CE9; + text-decoration: none; } a:visited { - color: #2E60A4; + color: #2E60A4; } a:hover { - color: red; - text-decoration: underline; + color: red; + text-decoration: underline; } table { - border-collapse: collapse; + border-collapse: collapse; } th { - font-weight: bold; - border: 1px solid lightgray; - background-color: #E5E5E5; - text-align: left; - vertical-align: top; - padding: 5px; + font-weight: bold; + border: 1px solid lightgray; + background-color: #E5E5E5; + text-align: left; + vertical-align: top; + padding: 5px; } td { - border: 1px solid lightgray; - padding: 5px; - vertical-align: top; + border: 1px solid lightgray; + padding: 5px; + vertical-align: top; } diff --git a/docs/dataset.html b/docs/dataset.html index be0ee783..e6e8310b 100644 --- a/docs/dataset.html +++ b/docs/dataset.html @@ -2,50 +2,50 @@ - vis.js | DataSet documentation + vis.js | DataSet documentation - - + + - +
-

DataSet documentation

+

DataSet documentation

-

Contents

- +

Contents

+ -

Overview

+

Overview

-

- 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. - 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. -

+

+ 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. + 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. +

-

Example

+

Example

-

- The following example shows how to use a DataSet. -

+

+ The following example shows how to use a DataSet. +

 // create a DataSet
@@ -55,15 +55,15 @@ var data = new vis.DataSet(options);
 // add items
 // note that the data items can contain different properties and data formats
 data.add([
-    {id: 1, text: 'item 1', date: new Date(2013, 6, 20), group: 1, first: true},
-    {id: 2, text: 'item 2', date: '2013-06-23', group: 2},
-    {id: 3, text: 'item 3', date: '2013-06-25', group: 2},
-    {id: 4, text: 'item 4'}
+  {id: 1, text: 'item 1', date: new Date(2013, 6, 20), group: 1, first: true},
+  {id: 2, text: 'item 2', date: '2013-06-23', group: 2},
+  {id: 3, text: 'item 3', date: '2013-06-25', group: 2},
+  {id: 4, text: 'item 4'}
 ]);
 
 // subscribe to any change in the DataSet
 data.subscribe('*', function (event, params, senderId) {
-    console.log('event', event, params);
+  console.log('event', event, params);
 });
 
 // update an existing item
@@ -82,94 +82,94 @@ console.log('item1', item1);
 
 // retrieve a filtered subset of the data
 var items = data.get({
-    filter: function (item) {
-        return item.group == 1;
-    }
+  filter: function (item) {
+    return item.group == 1;
+  }
 });
 console.log('filtered items', items);
 
 // retrieve formatted items
 var items = data.get({
-    fields: ['id', 'date'],
-    convert: {
-        date: 'ISODate'
-    }
+  fields: ['id', 'date'],
+  convert: {
+    date: 'ISODate'
+  }
 });
 console.log('formatted items', items);
 
-

Construction

+

Construction

-

- A DataSet can be constructed as: -

+

+ A DataSet can be constructed as: +

 var data = new vis.DataSet(options)
 
-

- After construction, data can be added to the DataSet using the methods - add and update, as described in section - Data Manipulation. -

- -

- The parameter options is optional and is an object which can - contain the following properties: -

- - - - - - - - - - - - - - - - - - - - -
NameTypeDefault valueDescription
fieldIdString"id" - The name of the field containing the id of the items. - - When data is fetched from a server which uses some specific - field to identify items, this field name can be specified - in the DataSet using the option fieldId. - For example CouchDB uses the field - "_id" to identify documents. -
convertObject.<String, String>none - An object containing field names as key, and data types as - value. By default, the type of the properties of items are left - unchanged. Item properties can be normalized by specifying a - field type. This is useful for example to automatically convert - stringified dates coming from a server into JavaScript Date - objects. The available data types are listed in section - Data Types. -
- - -

Data Manipulation

- -

- The data in a DataSet can be manipulated using the methods - add, - update, - and remove. - The DataSet can be emptied using the method - clear. -

+

+ After construction, data can be added to the DataSet using the methods + add and update, as described in section + Data Manipulation. +

+ +

+ The parameter options is optional and is an object which can + contain the following properties: +

+ + + + + + + + + + + + + + + + + + + + +
NameTypeDefault valueDescription
fieldIdString"id" + The name of the field containing the id of the items. + + When data is fetched from a server which uses some specific + field to identify items, this field name can be specified + in the DataSet using the option fieldId. + For example CouchDB uses the field + "_id" to identify documents. +
convertObject.<String, String>none + An object containing field names as key, and data types as + value. By default, the type of the properties of items are left + unchanged. Item properties can be normalized by specifying a + field type. This is useful for example to automatically convert + stringified dates coming from a server into JavaScript Date + objects. The available data types are listed in section + Data Types. +
+ + +

Data Manipulation

+ +

+ The data in a DataSet can be manipulated using the methods + add, + update, + and remove. + The DataSet can be emptied using the method + clear. +

 // create a DataSet
@@ -177,9 +177,9 @@ var data = new vis.DataSet();
 
 // add items
 data.add([
-    {id: 1, text: 'item 1'},
-    {id: 2, text: 'item 2'},
-    {id: 3, text: 'item 3'}
+  {id: 1, text: 'item 1'},
+  {id: 2, text: 'item 2'},
+  {id: 3, text: 'item 3'}
 ]);
 
 // update an item
@@ -189,193 +189,193 @@ data.update({id: 2, text: 'item 2 (updated)'});
 data.remove(3);
 
-

Add

- -

- Add a data item or an array with items. -

- - Syntax: -
var addedIds = DataSet.add(data [, senderId])
- - The argument data can contain: -
    -
  • - An Object containing a single item to be - added. The item must contain an id. -
  • -
  • - An Array or - google.visualization.DataTable containing - a list with items to be added. Each item must contain - an id. -
  • -
- -

- After the items are added to the DataSet, the DataSet will - trigger an event add. When a senderId - is provided, this id will be passed with the triggered - event to all subscribers. -

- -

- The method will throw an Error when an item with the same id - as any of the added items already exists. -

- -

Update

- -

- Update a data item or an array with items. -

- - Syntax: -
var updatedIds = DataSet.update(data [, senderId])
- - The argument data can contain: -
    -
  • - An Object containing a single item to be - updated. The item must contain an id. -
  • -
  • - An Array or - google.visualization.DataTable containing - a list with items to be updated. Each item must contain - an id. -
  • -
- -

- The provided properties will be merged in the existing item. - When an item does not exist, it will be created. -

- -

- After the items are updated, the DataSet will - trigger an event add for the added items, and - an event update. When a senderId - is provided, this id will be passed with the triggered - event to all subscribers. -

- -

Remove

- -

- Remove a data item or an array with items. -

- - Syntax: -
var removedIds = DataSet.remove(id [, senderId])
- -

- The argument id can be: -

-
    -
  • - A Number or String containing the id - of a single item to be removed. -
  • -
  • - An Object containing the item to be deleted. - The item will be deleted by its id. -
  • -
  • - An Array containing ids or items to be removed. -
  • -
- -

- The method ignores removal of non-existing items, and returns an array - containing the ids of the items which are actually removed from the - DataSet. -

- -

- After the items are removed, the DataSet will - trigger an event remove for the removed items. - When a senderId is provided, this id will be passed with - the triggered event to all subscribers. -

- - -

Clear

- -

- Clear the complete DataSet. -

- - Syntax: -
var removedIds = DataSet.clear([senderId])
- -

- After the items are removed, the DataSet will - trigger an event remove for all removed items. - When a senderId is provided, this id will be passed with - the triggered event to all subscribers. -

- - -

Data Filtering

- -

- Data can be retrieved from the DataSet using the method get. - This method can return a single item or a list with items. -

- -

A single item can be retrieved by its id:

+

Add

+ +

+ Add a data item or an array with items. +

+ +Syntax: +
var addedIds = DataSet.add(data [, senderId])
+ +The argument data can contain: +
    +
  • + An Object containing a single item to be + added. The item must contain an id. +
  • +
  • + An Array or + google.visualization.DataTable containing + a list with items to be added. Each item must contain + an id. +
  • +
+ +

+ After the items are added to the DataSet, the DataSet will + trigger an event add. When a senderId + is provided, this id will be passed with the triggered + event to all subscribers. +

+ +

+ The method will throw an Error when an item with the same id + as any of the added items already exists. +

+ +

Update

+ +

+ Update a data item or an array with items. +

+ +Syntax: +
var updatedIds = DataSet.update(data [, senderId])
+ +The argument data can contain: +
    +
  • + An Object containing a single item to be + updated. The item must contain an id. +
  • +
  • + An Array or + google.visualization.DataTable containing + a list with items to be updated. Each item must contain + an id. +
  • +
+ +

+ The provided properties will be merged in the existing item. + When an item does not exist, it will be created. +

+ +

+ After the items are updated, the DataSet will + trigger an event add for the added items, and + an event update. When a senderId + is provided, this id will be passed with the triggered + event to all subscribers. +

+ +

Remove

+ +

+ Remove a data item or an array with items. +

+ +Syntax: +
var removedIds = DataSet.remove(id [, senderId])
+ +

+ The argument id can be: +

+
    +
  • + A Number or String containing the id + of a single item to be removed. +
  • +
  • + An Object containing the item to be deleted. + The item will be deleted by its id. +
  • +
  • + An Array containing ids or items to be removed. +
  • +
+ +

+ The method ignores removal of non-existing items, and returns an array + containing the ids of the items which are actually removed from the + DataSet. +

+ +

+ After the items are removed, the DataSet will + trigger an event remove for the removed items. + When a senderId is provided, this id will be passed with + the triggered event to all subscribers. +

+ + +

Clear

+ +

+ Clear the complete DataSet. +

+ +Syntax: +
var removedIds = DataSet.clear([senderId])
+ +

+ After the items are removed, the DataSet will + trigger an event remove for all removed items. + When a senderId is provided, this id will be passed with + the triggered event to all subscribers. +

+ + +

Data Filtering

+ +

+ Data can be retrieved from the DataSet using the method get. + This method can return a single item or a list with items. +

+ +

A single item can be retrieved by its id:

 var item1 = dataset.get(1);
 
-

A selection of items can be retrieved by providing an array with ids:

+

A selection of items can be retrieved by providing an array with ids:

 var items = dataset.get([1, 3, 4]); // retrieve items 1, 3, and 4
 
-

All items can be retrieved by simply calling get without - specifying an id:

+

All items can be retrieved by simply calling get without + specifying an id:

 var items = dataset.get();          // retrieve all items
 
-

- Items can be filtered on specific properties by providing a filter - function. A filter function is executed for each of the items in the - DataSet, and is called with the item as parameter. The function must - return a boolean. All items for which the filter function returns - true will be emitted. -

+

+ Items can be filtered on specific properties by providing a filter + function. A filter function is executed for each of the items in the + DataSet, and is called with the item as parameter. The function must + return a boolean. All items for which the filter function returns + true will be emitted. +

 // retrieve all items having a property group with value 2
 var group2 = dataset.get({
-    filter: function (item) {
-        return (item.group == 2);
-    }
+  filter: function (item) {
+    return (item.group == 2);
+  }
 });
 
 // retrieve all items having a property balance with a value above zero
 var positiveBalance = dataset.get({
-    filter: function (item) {
-        return (item.balance > 0);
-    }
+  filter: function (item) {
+    return (item.balance > 0);
+  }
 });
 
 
-

Data Formatting

+

Data Formatting

-

- The DataSet contains functionality to format data retrieved via the - method get. The method get has the following - syntax: -

+

+ The DataSet contains functionality to format data retrieved via the + method get. The method get has the following + syntax: +

 var item  = DataSet.get(id, options);   // retrieve a single item
@@ -383,171 +383,171 @@ var items = DataSet.get(ids, options);  // retrieve a selection of items
 var items = DataSet.get(options);       // retrieve all items or a filtered set
 
-

- Where options is an Object which can have the following - properties: -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
fieldsString[ ] - An array with field names. - By default, all properties of the items are emitted. - When fields is defined, only the properties - whose name is specified in fields will be included - in the returned items. -
convertObject.<String, String> - An object containing field names as key, and data types as value. - By default, the type of the properties of an item are left - unchanged. When a field type is specified, this field in the - items will be converted to the specified type. This can be used - for example to convert ISO strings containing a date to a - JavaScript Date object, or convert strings to numbers or vice - versa. The available data types are listed in section - Data Types. -
filterFunctionItems can be filtered on specific properties by providing a filter - function. A filter function is executed for each of the items in the - DataSet, and is called with the item as parameter. The function must - return a boolean. All items for which the filter function returns - true will be emitted. - See section Data Filtering.
orderString | FunctionOrder the items by a field name or custom sort function.
- -

- The following example demonstrates formatting properties and filtering - properties from items. -

+

+ Where options is an Object which can have the following + properties: +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
fieldsString[ ] + An array with field names. + By default, all properties of the items are emitted. + When fields is defined, only the properties + whose name is specified in fields will be included + in the returned items. +
convertObject.<String, String> + An object containing field names as key, and data types as value. + By default, the type of the properties of an item are left + unchanged. When a field type is specified, this field in the + items will be converted to the specified type. This can be used + for example to convert ISO strings containing a date to a + JavaScript Date object, or convert strings to numbers or vice + versa. The available data types are listed in section + Data Types. +
filterFunctionItems can be filtered on specific properties by providing a filter + function. A filter function is executed for each of the items in the + DataSet, and is called with the item as parameter. The function must + return a boolean. All items for which the filter function returns + true will be emitted. + See section Data Filtering.
orderString | FunctionOrder the items by a field name or custom sort function.
+ +

+ The following example demonstrates formatting properties and filtering + properties from items. +

 // create a DataSet
 var data = new vis.DataSet();
 data.add([
-    {id: 1, text: 'item 1', date: '2013-06-20', group: 1, first: true},
-    {id: 2, text: 'item 2', date: '2013-06-23', group: 2},
-    {id: 3, text: 'item 3', date: '2013-06-25', group: 2},
-    {id: 4, text: 'item 4'}
+  {id: 1, text: 'item 1', date: '2013-06-20', group: 1, first: true},
+  {id: 2, text: 'item 2', date: '2013-06-23', group: 2},
+  {id: 3, text: 'item 3', date: '2013-06-25', group: 2},
+  {id: 4, text: 'item 4'}
 ]);
 
 // retrieve formatted items
 var items = data.get({
-    fields: ['id', 'date', 'group'],    // output the specified fields only
-    convert: {
-        date: 'Date',                   // convert the date fields to Date objects
-        group: 'String'                 // convert the group fields to Strings
-    }
+  fields: ['id', 'date', 'group'],    // output the specified fields only
+  convert: {
+    date: 'Date',                   // convert the date fields to Date objects
+    group: 'String'                 // convert the group fields to Strings
+  }
 });
 
-

Data Types

- -

- DataSet supports the following data types: -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameDescriptionExamples
BooleanA JavaScript Boolean - true
- false -
NumberA JavaScript Number - 32
- 2.4 -
StringA JavaScript String - "hello world"
- "2013-06-28" -
DateA JavaScript Date object - new Date()
- new Date(2013, 5, 28)
- new Date(1372370400000) -
MomentA Moment object, created with - moment.js - moment()
- moment('2013-06-28') -
ISODateA string containing an ISO Date - new Date().toISOString()
- "2013-06-27T22:00:00.000Z" -
ASPDateA string containing an ASP Date - "/Date(1372370400000)/"
- "/Date(1198908717056-0700)/" -
- - -

Subscriptions

- -

- One can subscribe on changes in a DataSet. - A subscription can be created using the method subscribe, - and removed with unsubscribe. -

+

Data Types

+ +

+ DataSet supports the following data types: +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameDescriptionExamples
BooleanA JavaScript Boolean + true
+ false +
NumberA JavaScript Number + 32
+ 2.4 +
StringA JavaScript String + "hello world"
+ "2013-06-28" +
DateA JavaScript Date object + new Date()
+ new Date(2013, 5, 28)
+ new Date(1372370400000) +
MomentA Moment object, created with + moment.js + moment()
+ moment('2013-06-28') +
ISODateA string containing an ISO Date + new Date().toISOString()
+ "2013-06-27T22:00:00.000Z" +
ASPDateA string containing an ASP Date + "/Date(1372370400000)/"
+ "/Date(1198908717056-0700)/" +
+ + +

Subscriptions

+ +

+ One can subscribe on changes in a DataSet. + A subscription can be created using the method subscribe, + and removed with unsubscribe. +

 // create a DataSet
@@ -555,7 +555,7 @@ var data = new vis.DataSet();
 
 // subscribe to any change in the DataSet
 data.subscribe('*', function (event, params, senderId) {
-    console.log('event:', event, 'params:', params, 'senderId:', senderId);
+  console.log('event:', event, 'params:', params, 'senderId:', senderId);
 });
 
 // add an item
@@ -565,144 +565,144 @@ data.remove(1);                                 // triggers an 'remove' event
 
-

Subscribe

- -

- Subscribe to an event. -

- - Syntax: -
DataSet.subscribe(event, callback)
- - Where: -
    -
  • - event is a String containing any of the events listed - in section Events. -
  • -
  • - callback is a callback function which will be called - each time the event occurs. The callback function is described in - section Callback. -
  • -
- -

Unsubscribe

- -

- Unsubscribe from an event. -

- - Syntax: -
DataSet.unsubscribe(event, callback)
- - Where event and callback correspond with the - parameters used to subscribe to the event. - -

Events

- -

- The following events are available for subscription: -

- - - - - - - - - - - - - - - - - - - - - - -
EventDescription
add - The add event is triggered when an item - or a set of items is added, or when an item is updated while - not yet existing. -
update - The update event is triggered when an existing item - or a set of existing items is updated. -
remove - The remove event is triggered when an item - or a set of items is removed. -
* - The * event is triggered when any of the events - add, update, and remove - occurs. -
- -

Callback

- -

- The callback functions of subscribers are called with the following - parameters: -

+

Subscribe

+ +

+ Subscribe to an event. +

+ +Syntax: +
DataSet.subscribe(event, callback)
+ +Where: +
    +
  • + event is a String containing any of the events listed + in section Events. +
  • +
  • + callback is a callback function which will be called + each time the event occurs. The callback function is described in + section Callback. +
  • +
+ +

Unsubscribe

+ +

+ Unsubscribe from an event. +

+ +Syntax: +
DataSet.unsubscribe(event, callback)
+ +Where event and callback correspond with the +parameters used to subscribe to the event. + +

Events

+ +

+ The following events are available for subscription: +

+ + + + + + + + + + + + + + + + + + + + + + +
EventDescription
add + The add event is triggered when an item + or a set of items is added, or when an item is updated while + not yet existing. +
update + The update event is triggered when an existing item + or a set of existing items is updated. +
remove + The remove event is triggered when an item + or a set of items is removed. +
* + The * event is triggered when any of the events + add, update, and remove + occurs. +
+ +

Callback

+ +

+ The callback functions of subscribers are called with the following + parameters: +

 function (event, params, senderId) {
-    // handle the event
+  // handle the event
 });
 
-

- where the parameters are defined as -

- - - - - - - - - - - - - - - - - - - - - - -
ParameterTypeDescription
eventString - Any of the available events: add, - update, or remove. -
paramsObject | null - Optional parameters providing more information on the event. - In case of the events add, - update, and remove, - params is always an object containing a property - items, which contains an array with the ids of the affected - items. -
senderIdString | Number - An senderId, optionally provided by the application code - which triggered the event. If senderId is not provided, the - argument will be null. -
- - - -

Data Policy

-

- All code and data is processed and rendered in the browser. - No data is sent to any server. -

+

+ where the parameters are defined as +

+ + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDescription
eventString + Any of the available events: add, + update, or remove. +
paramsObject | null + Optional parameters providing more information on the event. + In case of the events add, + update, and remove, + params is always an object containing a property + items, which contains an array with the ids of the affected + items. +
senderIdString | Number + An senderId, optionally provided by the application code + which triggered the event. If senderId is not provided, the + argument will be null. +
+ + + +

Data Policy

+

+ All code and data is processed and rendered in the browser. + No data is sent to any server. +

diff --git a/docs/dataview.html b/docs/dataview.html index bb459d9c..1698ffb1 100644 --- a/docs/dataview.html +++ b/docs/dataview.html @@ -2,69 +2,69 @@ - vis.js | DataView documentation + vis.js | DataView documentation - - + + - +
-

DataView documentation

+

DataView documentation

-

Contents

- +

Contents

+ -

Overview

+

Overview

-

- A DataView offers a filtered and/or formatted view on a - DataSet. - One can subscribe on changes in a DataView, and easily get filtered or - formatted data without having to specify filters and field types all - the time. -

+

+ A DataView offers a filtered and/or formatted view on a + DataSet. + One can subscribe on changes in a DataView, and easily get filtered or + formatted data without having to specify filters and field types all + the time. +

-

Example

+

Example

-

- The following example shows how to use a DataView. -

+

+ The following example shows how to use a DataView. +

 // create a DataSet
 var data = new vis.DataSet();
 data.add([
-    {id: 1, text: 'item 1', date: new Date(2013, 6, 20), group: 1, first: true},
-    {id: 2, text: 'item 2', date: '2013-06-23', group: 2},
-    {id: 3, text: 'item 3', date: '2013-06-25', group: 2},
-    {id: 4, text: 'item 4'}
+  {id: 1, text: 'item 1', date: new Date(2013, 6, 20), group: 1, first: true},
+  {id: 2, text: 'item 2', date: '2013-06-23', group: 2},
+  {id: 3, text: 'item 3', date: '2013-06-25', group: 2},
+  {id: 4, text: 'item 4'}
 ]);
 
 // create a DataView
 // the view will only contain items having a property group with value 1,
 // and will only output fields id, text, and date.
 var view = new vis.DataView(data, {
-    filter: function (item) {
-        return (item.group == 1);
-    },
-    fields: ['id', 'text', 'date']
+  filter: function (item) {
+    return (item.group == 1);
+  },
+  fields: ['id', 'text', 'date']
 });
 
 // subscribe to any change in the DataView
 view.subscribe('*', function (event, params, senderId) {
-    console.log('event', event, params);
+  console.log('event', event, params);
 });
 
 // update an item in the data set
@@ -78,131 +78,131 @@ console.log('ids', ids); // will output [1, 2]
 var items = view.get();
 
-

Construction

+

Construction

-

- A DataView can be constructed as: -

+

+ A DataView can be constructed as: +

 var data = new vis.DataView(dataset, options)
 
-

- where: -

- -
    -
  • - dataset is a DataSet or DataView. -
  • -
  • - options is an object which can - contain the following properties. Note that these properties - are exactly the same as the properties available in methods - DataSet.get and DataView.get. - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    NameTypeDescription
    convertObject.<String, String> - An object containing field names as key, and data types as value. - By default, the type of the properties of an item are left - unchanged. When a field type is specified, this field in the - items will be converted to the specified type. This can be used - for example to convert ISO strings containing a date to a - JavaScript Date object, or convert strings to numbers or vice - versa. The available data types are listed in section - Data Types. -
    fieldsString[ ] - An array with field names. - By default, all properties of the items are emitted. - When fields is defined, only the properties - whose name is specified in fields will be included - in the returned items. -
    filterfunctionItems can be filtered on specific properties by providing a filter - function. A filter function is executed for each of the items in the - DataSet, and is called with the item as parameter. The function must - return a boolean. All items for which the filter function returns - true will be emitted. - See also section Data Filtering.
    -
  • -
- -

Getting Data

- -

- Data of the DataView can be retrieved using the method get. -

+

+ where: +

+ +
    +
  • + dataset is a DataSet or DataView. +
  • +
  • + options is an object which can + contain the following properties. Note that these properties + are exactly the same as the properties available in methods + DataSet.get and DataView.get. + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescription
    convertObject.<String, String> + An object containing field names as key, and data types as value. + By default, the type of the properties of an item are left + unchanged. When a field type is specified, this field in the + items will be converted to the specified type. This can be used + for example to convert ISO strings containing a date to a + JavaScript Date object, or convert strings to numbers or vice + versa. The available data types are listed in section + Data Types. +
    fieldsString[ ] + An array with field names. + By default, all properties of the items are emitted. + When fields is defined, only the properties + whose name is specified in fields will be included + in the returned items. +
    filterfunctionItems can be filtered on specific properties by providing a filter + function. A filter function is executed for each of the items in the + DataSet, and is called with the item as parameter. The function must + return a boolean. All items for which the filter function returns + true will be emitted. + See also section Data Filtering.
    +
  • +
+ +

Getting Data

+ +

+ Data of the DataView can be retrieved using the method get. +

 var items = view.get();
 
-

- Data of a DataView can be filtered and formatted again, in exactly the - same way as in a DataSet. See sections - Data Filtering and - Data Formatting for more - information. -

+

+ Data of a DataView can be filtered and formatted again, in exactly the + same way as in a DataSet. See sections + Data Filtering and + Data Formatting for more + information. +

 var items = view.get({
-    fields: ['id', 'score'],
-    filter: function (item) {
-        return (item.score > 50);
-    }
+  fields: ['id', 'score'],
+  filter: function (item) {
+    return (item.score > 50);
+  }
 });
 
-

Subscriptions

-

- One can subscribe on changes in the DataView. Subscription works exactly - the same as for DataSets. See the documentation on - subscriptions in a DataSet - for more information. -

+

Subscriptions

+

+ One can subscribe on changes in the DataView. Subscription works exactly + the same as for DataSets. See the documentation on + subscriptions in a DataSet + for more information. +

 // create a DataSet and a view on the data set
 var data = new vis.DataSet();
 var view = new vis.DataView({
-    filter: function (item) {
-        return (item.group == 2);
-    }
+  filter: function (item) {
+    return (item.group == 2);
+  }
 });
 
 // subscribe to any change in the DataView
 view.subscribe('*', function (event, params, senderId) {
-    console.log('event:', event, 'params:', params, 'senderId:', senderId);
+  console.log('event:', event, 'params:', params, 'senderId:', senderId);
 });
 
 // add, update, and remove data in the DataSet...
@@ -210,11 +210,11 @@ view.subscribe('*', function (event, params, senderId) {
 
 
 
-    

Data Policy

-

- All code and data is processed and rendered in the browser. - No data is sent to any server. -

+

Data Policy

+

+ All code and data is processed and rendered in the browser. + No data is sent to any server. +

diff --git a/docs/graph.html b/docs/graph.html index f196a732..fec8affb 100644 --- a/docs/graph.html +++ b/docs/graph.html @@ -2,12 +2,12 @@ - vis.js | graph documentation + vis.js | graph documentation - - + + - + @@ -17,52 +17,52 @@

Contents

Overview

- Graph is a visualization to display graphs and networks consisting of nodes - and edges. The visualization is easy to use and supports custom shapes, - styles, colors, sizes, images, and more. + Graph is a visualization to display graphs and networks consisting of nodes + and edges. The visualization is easy to use and supports custom shapes, + styles, colors, sizes, images, and more.

- The graph visualization works smooth on any modern browser for up to a - few hundred nodes and edges. + The graph visualization works smooth on any modern browser for up to a + few hundred nodes and edges.

- To get started with Graph, install or download the - vis.js library. + To get started with Graph, install or download the + vis.js library.

Example

- Here a basic graph example. More examples can be found in the - examples directory. + Here a basic graph example. More examples can be found in the + examples directory.

<!doctype html>
 <html>
 <head>
-    <title>Graph | Basic usage</title>
+  <title>Graph | Basic usage</title>
 
-    <script type="text/javascript" src="../../vis.js"></script>
+  <script type="text/javascript" src="../../vis.js"></script>
 </head>
 
 <body>
@@ -70,34 +70,34 @@
 <div id="mygraph"></div>
 
 <script type="text/javascript">
-    // create an array with nodes
-    var nodes = [
-        {id: 1, label: 'Node 1'},
-        {id: 2, label: 'Node 2'},
-        {id: 3, label: 'Node 3'},
-        {id: 4, label: 'Node 4'},
-        {id: 5, label: 'Node 5'}
-    ];
-
-    // create an array with edges
-    var edges = [
-        {from: 1, to: 2},
-        {from: 1, to: 3},
-        {from: 2, to: 4},
-        {from: 2, to: 5}
-    ];
-
-    // create a graph
-    var container = document.getElementById('mygraph');
-    var data= {
-        nodes: nodes,
-        edges: edges,
-    };
-    var options = {
-        width: '400px',
-        height: '400px'
-    };
-    var graph = new vis.Graph(container, data, options);
+  // create an array with nodes
+  var nodes = [
+    {id: 1, label: 'Node 1'},
+    {id: 2, label: 'Node 2'},
+    {id: 3, label: 'Node 3'},
+    {id: 4, label: 'Node 4'},
+    {id: 5, label: 'Node 5'}
+  ];
+
+  // create an array with edges
+  var edges = [
+    {from: 1, to: 2},
+    {from: 1, to: 3},
+    {from: 2, to: 4},
+    {from: 2, to: 5}
+  ];
+
+  // create a graph
+  var container = document.getElementById('mygraph');
+  var data= {
+    nodes: nodes,
+    edges: edges,
+  };
+  var options = {
+    width: '400px',
+    height: '400px'
+  };
+  var graph = new vis.Graph(container, data, options);
 </script>
 
 </body>
@@ -107,8 +107,8 @@
 
 

Loading

- Install or download the vis.js library. - in a subfolder of your project. Include the library script in the head of your html code: + Install or download the vis.js library. + in a subfolder of your project. Include the library script in the head of your html code:

@@ -121,278 +121,278 @@ The constructor of the Graph is vis.Graph.
 
 The constructor accepts three parameters:
 
    -
  • - container is the DOM element in which to create the graph. -
  • -
  • - data is an Object containing properties nodes and - edges, which both contain an array with objects. - Optionally, data may contain an options object. - The parameter data is optional, data can also be set using - the method setData. Section Data Format - describes the data object. -
  • -
  • - options is an optional Object containing a name-value map - with options. Options can also be set using the method - setOptions. - Section Configuration Options - describes the available options. -
  • +
  • + container is the DOM element in which to create the graph. +
  • +
  • + data is an Object containing properties nodes and + edges, which both contain an array with objects. + Optionally, data may contain an options object. + The parameter data is optional, data can also be set using + the method setData. Section Data Format + describes the data object. +
  • +
  • + options is an optional Object containing a name-value map + with options. Options can also be set using the method + setOptions. + Section Configuration Options + describes the available options. +

Data Format

- The data parameter of the Graph constructor is an object - which can contain different types of data. - The following properties are supported in the data object: + The data parameter of the Graph constructor is an object + which can contain different types of data. + The following properties are supported in the data object:

    -
  • - A property pair nodes and edges, - both containing an Array with objects. The data formats are described - in the sections Nodes and Edges. - Example: +
  • + A property pair nodes and edges, + both containing an Array with objects. The data formats are described + in the sections Nodes and Edges. + Example:
     var data = {
    -    nodes: [...],
    -    edges: [...]
    +  nodes: [...],
    +  edges: [...]
     };
     
    -
  • -
  • - A property dot, - containing a string with data in the - DOT language. - DOT support is described in section DOT_language. - - Example: +
  • +
  • + A property dot, + containing a string with data in the + DOT language. + DOT support is described in section DOT_language. + + Example:
     var data = {
    -    dot: '...'
    +  dot: '...'
     };
     
    -
  • -
  • - A property options, - containing an object with global options. - Options can be provided as third parameter in the graph constructor - as well. Section Configuration Options - describes the available options. - -
  • + +
  • + A property options, + containing an object with global options. + Options can be provided as third parameter in the graph constructor + as well. Section Configuration Options + describes the available options. + +

Nodes

- Nodes typically have an id and label. - A node must contain at least a property id. - Nodes can have extra properties, used to define the shape and style of the - nodes. + Nodes typically have an id and label. + A node must contain at least a property id. + Nodes can have extra properties, used to define the shape and style of the + nodes.

- A JavaScript Array with nodes is constructed like: + A JavaScript Array with nodes is constructed like:

 var nodes = [
-    {
-        id: 1,
-        label: 'Node 1'
-    },
-    // ... more nodes
+  {
+    id: 1,
+    label: 'Node 1'
+  },
+  // ... more nodes
 ];
 

- Nodes support the following properties: + Nodes support the following properties:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeRequiredDescription
colorString | ObjectnoColor for the node.
color.backgroundStringnoBackground color for the node.
color.borderStringnoBorder color for the node.
color.highlightString | ObjectnoColor of the node when selected.
color.highlight.backgroundStringnoBackground color of the node when selected.
color.highlight.borderStringnoBorder color of the node when selected.
groupNumber | StringnoA group number or name. The type can be number, - string, or an other type. All nodes with the same group get - the same color schema.
fontColorStringnoFont color for label in the node.
fontFaceStringnoFont face for label in the node, for example "verdana" or "arial".
fontSizeNumbernoFont size in pixels for label in the node.
idNumber | StringyesA unique id for this node. - Nodes may not have duplicate id's. - Id's do not need to be consecutive. - An id is normally a number, but may be any type.
imagestringnoUrl of an image. Only applicable when the shape of the node is - image.
radiusnumbernoRadius for the node. Applicable for all shapes except box, - circle, ellipse and database. - The value of radius will override a value in - property value.
shapestringnoDefine the shape for the node. - Choose from - ellipse (default), circle, box, - database, image, label, dot, - star, triangle, triangleDown, and square. -

- - In case of image, a property with name image must - be provided, containing image urls. -

- - The shapes dot, star, triangle, - triangleDown, and square, are scalable. - The size is determined by the properties radius or - value. -

- - When a property label is provided, - this label will be displayed inside the shape in case of shapes - box, circle, ellipse, - and database. - For all other shapes, the label will be displayed right below the shape. - -
labelstringnoText label to be displayed in the node or under the image of the node. - Multiple lines can be separated by a newline character \n .
titlestringnoTitle to be displayed when the user hovers over the node. - The title can contain HTML code.
valuenumbernoA value for the node. - The radius of the nodes will be scaled automatically from minimum to - maximum value. - Only applicable when the shape of the node is dot. - If a radius is provided for the node too, it will override the - radius calculated from the value.
xnumbernoHorizontal position in pixels. - The horizontal position of the node will be fixed. - The vertical position y may remain undefined.
ynumbernoVertical position in pixels. - The vertical position of the node will be fixed. - The horizontal position x may remain undefined.
NameTypeRequiredDescription
colorString | ObjectnoColor for the node.
color.backgroundStringnoBackground color for the node.
color.borderStringnoBorder color for the node.
color.highlightString | ObjectnoColor of the node when selected.
color.highlight.backgroundStringnoBackground color of the node when selected.
color.highlight.borderStringnoBorder color of the node when selected.
groupNumber | StringnoA group number or name. The type can be number, + string, or an other type. All nodes with the same group get + the same color schema.
fontColorStringnoFont color for label in the node.
fontFaceStringnoFont face for label in the node, for example "verdana" or "arial".
fontSizeNumbernoFont size in pixels for label in the node.
idNumber | StringyesA unique id for this node. + Nodes may not have duplicate id's. + Id's do not need to be consecutive. + An id is normally a number, but may be any type.
imagestringnoUrl of an image. Only applicable when the shape of the node is + image.
radiusnumbernoRadius for the node. Applicable for all shapes except box, + circle, ellipse and database. + The value of radius will override a value in + property value.
shapestringnoDefine the shape for the node. + Choose from + ellipse (default), circle, box, + database, image, label, dot, + star, triangle, triangleDown, and square. +

+ + In case of image, a property with name image must + be provided, containing image urls. +

+ + The shapes dot, star, triangle, + triangleDown, and square, are scalable. + The size is determined by the properties radius or + value. +

+ + When a property label is provided, + this label will be displayed inside the shape in case of shapes + box, circle, ellipse, + and database. + For all other shapes, the label will be displayed right below the shape. + +
labelstringnoText label to be displayed in the node or under the image of the node. + Multiple lines can be separated by a newline character \n .
titlestringnoTitle to be displayed when the user hovers over the node. + The title can contain HTML code.
valuenumbernoA value for the node. + The radius of the nodes will be scaled automatically from minimum to + maximum value. + Only applicable when the shape of the node is dot. + If a radius is provided for the node too, it will override the + radius calculated from the value.
xnumbernoHorizontal position in pixels. + The horizontal position of the node will be fixed. + The vertical position y may remain undefined.
ynumbernoVertical position in pixels. + The vertical position of the node will be fixed. + The horizontal position x may remain undefined.
@@ -400,176 +400,176 @@ var nodes = [

Edges

- Edges are connections between nodes. - An edge must at least contain properties from and - to, both referring to the id of a node. - Edges can have extra properties, used to define the type and style. + Edges are connections between nodes. + An edge must at least contain properties from and + to, both referring to the id of a node. + Edges can have extra properties, used to define the type and style.

- A JavaScript Array with edges is constructed as: + A JavaScript Array with edges is constructed as:

 var edges = [
-    {
-        from: 1,
-        to: 3
-    },
-    // ... more edges
+  {
+    from: 1,
+    to: 3
+  },
+  // ... more edges
 ];
 

- Edges support the following properties: + Edges support the following properties:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeRequiredDescription
colorstringnoA HTML color for the edge.
dashObjectno - Object containing properties for dashed lines. - Available properties: length, gap, - altLength. -
dash.altLengthnumbernoLength of the alternated dash in pixels on a dashed line. - Specifying dash.altLength allows for creating - a dashed line with a dash-dot style, for example when - dash.length=10 and dash.altLength=5. - See also the option dahs.length. - Only applicable when the line style is dash-line.
dash.lengthnumbernoLength of a dash in pixels on a dashed line. - Only applicable when the line style is dash-line.
dash.gapnumbernoLength of a gap in pixels on a dashed line. - Only applicable when the line style is dash-line.
fontColorStringnoFont color for the text label of the edge. - Only applicable when property label is defined.
fontFaceStringnoFont face for the text label of the edge, - for example "verdana" or "arial". - Only applicable when property label is defined.
fontSizeNumbernoFont size in pixels for the text label of the edge. - Only applicable when property label is defined.
fromNumber | StringyesThe id of a node where the edge starts. The type must correspond with - the type of the node id's. This is normally a number, but can be any - type.
lengthnumbernoThe length of the edge in pixels.
stylestringnoDefine a line style for the edge. - Choose from line (default), arrow, - arrow-center, or dash-line. -
labelstringnoText label to be displayed halfway the edge.
titlestringnoTitle to be displayed when the user hovers over the edge. - The title can contain HTML code.
toNumber | StringyesThe id of a node where the edge ends. The type must correspond with - the type of the node id's. This is normally a number, but can be any - type.
valuenumbernoA value for the edge. - The width of the edges will be scaled automatically from minimum to - maximum value. - If a width is provided for the edge too, it will override the - width calculated from the value.
widthnumbernoWidth of the line in pixels. The width will - override a specified value, if a value is - specified too.
NameTypeRequiredDescription
colorstringnoA HTML color for the edge.
dashObjectno + Object containing properties for dashed lines. + Available properties: length, gap, + altLength. +
dash.altLengthnumbernoLength of the alternated dash in pixels on a dashed line. + Specifying dash.altLength allows for creating + a dashed line with a dash-dot style, for example when + dash.length=10 and dash.altLength=5. + See also the option dahs.length. + Only applicable when the line style is dash-line.
dash.lengthnumbernoLength of a dash in pixels on a dashed line. + Only applicable when the line style is dash-line.
dash.gapnumbernoLength of a gap in pixels on a dashed line. + Only applicable when the line style is dash-line.
fontColorStringnoFont color for the text label of the edge. + Only applicable when property label is defined.
fontFaceStringnoFont face for the text label of the edge, + for example "verdana" or "arial". + Only applicable when property label is defined.
fontSizeNumbernoFont size in pixels for the text label of the edge. + Only applicable when property label is defined.
fromNumber | StringyesThe id of a node where the edge starts. The type must correspond with + the type of the node id's. This is normally a number, but can be any + type.
lengthnumbernoThe length of the edge in pixels.
stylestringnoDefine a line style for the edge. + Choose from line (default), arrow, + arrow-center, or dash-line. +
labelstringnoText label to be displayed halfway the edge.
titlestringnoTitle to be displayed when the user hovers over the edge. + The title can contain HTML code.
toNumber | StringyesThe id of a node where the edge ends. The type must correspond with + the type of the node id's. This is normally a number, but can be any + type.
valuenumbernoA value for the edge. + The width of the edges will be scaled automatically from minimum to + maximum value. + If a width is provided for the edge too, it will override the + width calculated from the value.
widthnumbernoWidth of the line in pixels. The width will + override a specified value, if a value is + specified too.
@@ -577,20 +577,20 @@ var edges = [

DOT language

- Graph supports data in the - DOT language. - To provide data in the DOT language, the data object must contain - a property dot with a String containing the data. + Graph supports data in the + DOT language. + To provide data in the DOT language, the data object must contain + a property dot with a String containing the data.

- Example usage: + Example usage:

 // provide data in the DOT language
 var data = {
-    dot: 'digraph {1 -> 1 -> 2; 2 -> 3; 2 -- 4; 2 -> 1 }'
+  dot: 'digraph {1 -> 1 -> 2; 2 -> 3; 2 -- 4; 2 -> 1 }'
 };
 
 // create a graph
@@ -602,252 +602,252 @@ var graph = new vis.Graph(container, data);
 

Configuration Options

- Options can be used to customize the graph. Options are defined as a JSON object. - All options are optional. + Options can be used to customize the graph. Options are defined as a JSON object. + All options are optional.

 var options = {
-    width:  '100%',
-    height: '400px',
-    edges: {
-        color: 'red',
-        width: 2
-    }
+  width:  '100%',
+  height: '400px',
+  edges: {
+    color: 'red',
+    width: 2
+  }
 };
 

- The following options are available. + The following options are available.

- - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + - - - - + + + + - - - - + + + + - - - - + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + +
NameTypeDefaultDescriptionNameTypeDefaultDescription
edges.colorString"#2B7CE9"The default color of a edge.edges.colorString"#2B7CE9"The default color of a edge.
edges.dashObjectObject - Object containing default properties for dashed lines. - Available properties: length, gap, - altLength. - edges.dashObjectObject + Object containing default properties for dashed lines. + Available properties: length, gap, + altLength. +
edges.dash.altLengthnumbernoneDefault length of the alternated dash in pixels on a dashed line. - Specifying dash.altLength allows for creating - a dashed line with a dash-dot style, for example when - dash.length=10 and dash.altLength=5. - See also the option dahs.length. - Only applicable when the line style is dash-line.edges.dash.altLengthnumbernoneDefault length of the alternated dash in pixels on a dashed line. + Specifying dash.altLength allows for creating + a dashed line with a dash-dot style, for example when + dash.length=10 and dash.altLength=5. + See also the option dahs.length. + Only applicable when the line style is dash-line.
edges.dash.lengthnumber10Default length of a dash in pixels on a dashed line. - Only applicable when the line style is dash-line.edges.dash.lengthnumber10Default length of a dash in pixels on a dashed line. + Only applicable when the line style is dash-line.
edges.dash.gapnumber5Default length of a gap in pixels on a dashed line. - Only applicable when the line style is dash-line.edges.dash.gapnumber5Default length of a gap in pixels on a dashed line. + Only applicable when the line style is dash-line.
edges.lengthNumber100The default length of a edge.edges.lengthNumber100The default length of a edge.
edges.styleString"line"The default style of a edge. - Choose from line (default), arrow, - arrow-center, dash-line.edges.styleString"line"The default style of a edge. + Choose from line (default), arrow, + arrow-center, dash-line.
edges.widthNumber1The default width of a edge.edges.widthNumber1The default width of a edge.
groupsObjectnoneIt is possible to specify custom styles for groups. - Each node assigned a group gets the specified style. - See Groups for an overview of the available styles - and an example. - groupsObjectnoneIt is possible to specify custom styles for groups. + Each node assigned a group gets the specified style. + See Groups for an overview of the available styles + and an example. +
heightString"400px"The height of the graph in pixels or as a percentage.heightString"400px"The height of the graph in pixels or as a percentage.
nodes.colorString | ObjectObjectDefault color of the nodes. When color is a string, the color is applied + nodes.colorString | ObjectObjectDefault color of the nodes. When color is a string, the color is applied to both background as well as the border of the node.
nodes.color.backgroundString"#97C2FC"Default background color of the nodesnodes.color.backgroundString"#97C2FC"Default background color of the nodes
nodes.color.borderString"#2B7CE9"Default border color of the nodesnodes.color.borderString"#2B7CE9"Default border color of the nodes
nodes.color.highlightString | ObjectObjectDefault color of the node when the node is selected. Applied to + nodes.color.highlightString | ObjectObjectDefault color of the node when the node is selected. Applied to both border and background of the node.
nodes.color.highlight.backgroundString"#D2E5FF"Default background color of the node when selected.nodes.color.highlight.backgroundString"#D2E5FF"Default background color of the node when selected.
nodes.color.highlight.borderString"#2B7CE9"Default border color of the node when selected.nodes.color.highlight.borderString"#2B7CE9"Default border color of the node when selected.
nodes.fontColorString"black"Default font color for the text label in the nodes.nodes.fontColorString"black"Default font color for the text label in the nodes.
nodes.fontFaceString"sans"Default font face for the text label in the nodes, for example "verdana" or "arial".nodes.fontFaceString"sans"Default font face for the text label in the nodes, for example "verdana" or "arial".
nodes.fontSizeNumber14Default font size in pixels for the text label in the nodes.nodes.fontSizeNumber14Default font size in pixels for the text label in the nodes.
nodes.groupStringnoneDefault group for the nodes.nodes.groupStringnoneDefault group for the nodes.
nodes.imageStringnoneDefault image url for the nodes. only applicable with shape image.nodes.imageStringnoneDefault image url for the nodes. only applicable with shape image.
nodes.widthMinNumber16The minimum width for a scaled image. Only applicable with shape image.nodes.widthMinNumber16The minimum width for a scaled image. Only applicable with shape image.
nodes.widthMaxNumber64The maximum width for a scaled image. Only applicable with shape image.nodes.widthMaxNumber64The maximum width for a scaled image. Only applicable with shape image.
nodes.shapeString"ellipse"The default shape for all nodes. - Choose from - ellipse (default), circle, box, - database, image, label, dot, - star, triangle, triangleDown, and square. - This shape can be overridden by a group shape, or by a shape of an individual node.nodes.shapeString"ellipse"The default shape for all nodes. + Choose from + ellipse (default), circle, box, + database, image, label, dot, + star, triangle, triangleDown, and square. + This shape can be overridden by a group shape, or by a shape of an individual node.
nodes.radiusNumber5The default radius for a node. Only applicable with shape dot.nodes.radiusNumber5The default radius for a node. Only applicable with shape dot.
nodes.radiusMinNumber5The minimum radius for a scaled node. Only applicable with shape dot.nodes.radiusMinNumber5The minimum radius for a scaled node. Only applicable with shape dot.
nodes.radiusMaxNumber20The maximum radius for a scaled node. Only applicable with shape dot.nodes.radiusMaxNumber20The maximum radius for a scaled node. Only applicable with shape dot.
selectableBooleantrueIf true, nodes in the graph can be selected by clicking them. - Long press can be used to select multiple nodes.selectableBooleantrueIf true, nodes in the graph can be selected by clicking them. + Long press can be used to select multiple nodes.
stabilizeBooleantrueIf true, the graph is stabilized before displaying it. If false, - the nodes move to a stabe position visibly in an animated way.stabilizeBooleantrueIf true, the graph is stabilized before displaying it. If false, + the nodes move to a stabe position visibly in an animated way.
widthString"400px"The width of the graph in pixels or as a percentage.widthString"400px"The width of the graph in pixels or as a percentage.
@@ -857,254 +857,254 @@ var options = {

Groups

It is possible to specify custom styles for groups of nodes. - Each node having assigned to this group gets the specified style. - The options groups is an object containing one or multiple groups, - identified by a unique string, the groupname. + Each node having assigned to this group gets the specified style. + The options groups is an object containing one or multiple groups, + identified by a unique string, the groupname.

- A group can have the following styles: + A group can have the following styles:

 var options = {
-    // ...
-
-    groups: {
-        mygroup: {
-            shape: 'circle',
-            color: {
-                border: 'black',
-                background: 'white',
-                highlight: {
-                    border: 'yellow',
-                    background: 'orange'
-                }
-            }
-            fontColor: 'red',
-            fontSize: 18
+  // ...
+
+  groups: {
+    mygroup: {
+      shape: 'circle',
+      color: {
+        border: 'black',
+        background: 'white',
+        highlight: {
+          border: 'yellow',
+          background: 'orange'
         }
-        // add more groups here
+      }
+      fontColor: 'red',
+      fontSize: 18
     }
+    // add more groups here
+  }
 };
 
 var nodes = [
-    {id: 1, label: 'Node 1'},                    // will get the default style
-    {id: 2, label: 'Node 2', group: 'mygroup'},  // will get the style from 'mygroup'
-    // ... more nodes
+  {id: 1, label: 'Node 1'},                    // will get the default style
+  {id: 2, label: 'Node 2', group: 'mygroup'},  // will get the style from 'mygroup'
+  // ... more nodes
 ];
 

The following styles are available for groups:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDefaultDescription
colorString | ObjectObjectColor of the node
color.borderString"#2B7CE9"Border color of the node
color.backgroundString"#97C2FC"Background color of the node
color.highlightString"#D2E5FF"Color of the node when selected.
color.highlight.backgroundString"#D2E5FF"Background color of the node when selected.
color.highlight.borderString"#D2E5FF"Border color of the node when selected.
imageStringnoneDefault image for the nodes. Only applicable in combination with - shape image.
fontColorString"black"Font color of the node.
fontFaceString"sans"Font name of the node, for example "verdana" or "arial".
fontSizeNumber14Font size for the node in pixels.
shapeString"ellipse"Choose from - ellipse (default), circle, box, - database, image, label, dot, - star, triangle, triangleDown, and square. - In case of image, a property with name image must be provided, containing - image urls.
radiusNumber5Default radius for the node. Only applicable in combination with - shapes box and dot.
NameTypeDefaultDescription
colorString | ObjectObjectColor of the node
color.borderString"#2B7CE9"Border color of the node
color.backgroundString"#97C2FC"Background color of the node
color.highlightString"#D2E5FF"Color of the node when selected.
color.highlight.backgroundString"#D2E5FF"Background color of the node when selected.
color.highlight.borderString"#D2E5FF"Border color of the node when selected.
imageStringnoneDefault image for the nodes. Only applicable in combination with + shape image.
fontColorString"black"Font color of the node.
fontFaceString"sans"Font name of the node, for example "verdana" or "arial".
fontSizeNumber14Font size for the node in pixels.
shapeString"ellipse"Choose from + ellipse (default), circle, box, + database, image, label, dot, + star, triangle, triangleDown, and square. + In case of image, a property with name image must be provided, containing + image urls.
radiusNumber5Default radius for the node. Only applicable in combination with + shapes box and dot.

Methods

- Graph supports the following methods. + Graph supports the following methods.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodReturn TypeDescription
setData(data)noneLoads data. Parameter data is an object containing - nodes, edges, and options. Parameters nodes, edges are an Array. - Options is a name-value map and is optional. -
setOptions(options)noneSet options for the graph. The available options are described in - the section Configuration Options. -
getSelection()Array of idsReturns an array with the ids of the selected nodes. - Returns an empty array if no nodes are selected. - The selections are not ordered. -
redraw()noneRedraw the graph. Useful when the layout of the webpage changed.
setSelection(selection)noneSelect nodes. - selection is an array with ids of nodes to be selected. - The array selection can contain zero or multiple ids. - Example usage: graph.setSelection([3, 5]); will select - nodes with id 3 and 5. -
setSize(width, height)noneParameters width and height are strings, - containing a new size for the visualization. Size can be provided in pixels - or in percentages.
MethodReturn TypeDescription
setData(data)noneLoads data. Parameter data is an object containing + nodes, edges, and options. Parameters nodes, edges are an Array. + Options is a name-value map and is optional. +
setOptions(options)noneSet options for the graph. The available options are described in + the section Configuration Options. +
getSelection()Array of idsReturns an array with the ids of the selected nodes. + Returns an empty array if no nodes are selected. + The selections are not ordered. +
redraw()noneRedraw the graph. Useful when the layout of the webpage changed.
setSelection(selection)noneSelect nodes. + selection is an array with ids of nodes to be selected. + The array selection can contain zero or multiple ids. + Example usage: graph.setSelection([3, 5]); will select + nodes with id 3 and 5. +
setSize(width, height)noneParameters width and height are strings, + containing a new size for the visualization. Size can be provided in pixels + or in percentages.

Events

- Graph fires events after one or multiple nodes are selected. - The event can be catched by creating a listener. + Graph fires events after one or multiple nodes are selected. + The event can be catched by creating a listener.

- Here an example on how to catch a select event. + Here an example on how to catch a select event.

 function onSelect() {
-    alert('selected nodes: ' + graph.getSelection());
+  alert('selected nodes: ' + graph.getSelection());
 }
 
 vis.events.addListener(graph, 'select', onSelect);
 

- The following events are available. + The following events are available.

- - - - - - - - - - - - - - - + + + + + + + + + + + + + + +
nameDescriptionProperties
selectFired after the user selects or unselects a node by clicking it, - or when selecting a number of nodes by dragging a selection area - around them. Not fired when the method setSelection - is executed. The ids of the selected nodes can be retrieved via the - method getSelection. - none
nameDescriptionProperties
selectFired after the user selects or unselects a node by clicking it, + or when selecting a number of nodes by dragging a selection area + around them. Not fired when the method setSelection + is executed. The ids of the selected nodes can be retrieved via the + method getSelection. + none

Data Policy

- All code and data is processed and rendered in the browser. - No data is sent to any server. + All code and data is processed and rendered in the browser. + No data is sent to any server.

diff --git a/docs/index.html b/docs/index.html index 6f907a0f..8be44e9c 100644 --- a/docs/index.html +++ b/docs/index.html @@ -2,201 +2,201 @@ - vis.js | documentation + vis.js | documentation - - + + - +
-

vis.js documentation

- -

- Vis.js is a dynamic, browser based visualization library. - The library is designed to be easy to use, handle large amounts - of dynamic data, and enable manipulation of the data. -

- -

- The library is developed by - Almende B.V. -

- -

Components

- -

- Vis.js contains of the following components: -

- -
    -
  • - DataSet. - A flexible key/value based data set. - Add, update, and remove items. Subscribe on changes in the data set. - A DataSet can filter and order items, and convert fields of items. -
  • -
  • - DataView. - A filtered and/or formatted view on a DataSet. -
  • -
  • - Graph. - Display a graph or network with nodes and edges. -
  • -
  • - Timeline. - Display different types of data on a timeline. The timeline and the - items on the timeline can be interactively moved, zoomed, and - manipulated. -
  • -
- - - - -

Install

- -

npm

+

vis.js documentation

+ +

+ Vis.js is a dynamic, browser based visualization library. + The library is designed to be easy to use, handle large amounts + of dynamic data, and enable manipulation of the data. +

+ +

+ The library is developed by + Almende B.V. +

+ +

Components

+ +

+ Vis.js contains of the following components: +

+ +
    +
  • + DataSet. + A flexible key/value based data set. + Add, update, and remove items. Subscribe on changes in the data set. + A DataSet can filter and order items, and convert fields of items. +
  • +
  • + DataView. + A filtered and/or formatted view on a DataSet. +
  • +
  • + Graph. + Display a graph or network with nodes and edges. +
  • +
  • + Timeline. + Display different types of data on a timeline. The timeline and the + items on the timeline can be interactively moved, zoomed, and + manipulated. +
  • +
+ + + + +

Install

+ +

npm

 npm install vis
 
-

bower

+

bower

 bower install vis
 
-

download

- Download the library from the website: - http://visjs.org. +

download

+ Download the library from the website: + http://visjs.org. -

Load

+

Load

-

- To use a component, include the javascript file of vis in your web page: -

+

+ To use a component, include the javascript file of vis in your web page: +

<!DOCTYPE HTML>
 <html>
 <head>
-    <script src="components/vis/vis.js"></script>
+  <script src="components/vis/vis.js"></script>
 </head>
 <body>
 <script type="text/javascript">
-    // ... load a visualization
+  // ... load a visualization
 </script>
 </body>
 </html>
 
-

- or load vis.js using require.js: -

+

+ or load vis.js using require.js: +

 require.config({
-    paths: {
-        vis: 'path/to/vis',
-    }
+  paths: {
+    vis: 'path/to/vis',
+  }
 });
 
 require(['vis'], function (math) {
-    // ... load a visualization
+  // ... load a visualization
 });
 
-

- A timeline can be instantiated as follows. Other components can be - created in a similar way. -

+

+ A timeline can be instantiated as follows. Other components can be + created in a similar way. +

 var timeline = new vis.Timeline(container, data, options);
 
-

- Where container is an HTML element, data is - an Array with data or a DataSet, and options is an optional - object with configuration options for the component. -

+

+ Where container is an HTML element, data is + an Array with data or a DataSet, and options is an optional + object with configuration options for the component. +

-

Use

+

Use

-

+

A basic example on using a Timeline is shown below. More examples can be found in the examples directory of the project. -

+ target="_blank">examples directory of the project. +

 <!DOCTYPE HTML>
 <html>
 <head>
-    <title>Timeline basic demo</title>
-    <script src="components/vis/vis.js"></script>
-
-    <style type="text/css">
-        body, html {
-            font-family: sans-serif;
-        }
-    </style>
+  <title>Timeline basic demo</title>
+  <script src="components/vis/vis.js"></script>
+
+  <style type="text/css">
+    body, html {
+      font-family: sans-serif;
+    }
+  </style>
 </head>
 <body>
 <div id="visualization"></div>
 
 <script type="text/javascript">
-    var container = document.getElementById('visualization');
-    var data = [
-        {id: 1, content: 'item 1', start: '2013-04-20'},
-        {id: 2, content: 'item 2', start: '2013-04-14'},
-        {id: 3, content: 'item 3', start: '2013-04-18'},
-        {id: 4, content: 'item 4', start: '2013-04-16', end: '2013-04-19'},
-        {id: 5, content: 'item 5', start: '2013-04-25'},
-        {id: 6, content: 'item 6', start: '2013-04-27'}
-    ];
-    var options = {};
-    var timeline = new vis.Timeline(container, data, options);
+  var container = document.getElementById('visualization');
+  var data = [
+    {id: 1, content: 'item 1', start: '2013-04-20'},
+    {id: 2, content: 'item 2', start: '2013-04-14'},
+    {id: 3, content: 'item 3', start: '2013-04-18'},
+    {id: 4, content: 'item 4', start: '2013-04-16', end: '2013-04-19'},
+    {id: 5, content: 'item 5', start: '2013-04-25'},
+    {id: 6, content: 'item 6', start: '2013-04-27'}
+  ];
+  var options = {};
+  var timeline = new vis.Timeline(container, data, options);
 </script>
 </body>
 </html>
 
-

License

+

License

-

- Copyright (C) 2010-2013 Almende B.V. -

+

+ Copyright (C) 2010-2013 Almende B.V. +

-

- Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at -

+

+ Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at +

-

- http://www.apache.org/licenses/LICENSE-2.0 -

+

+ http://www.apache.org/licenses/LICENSE-2.0 +

-

- Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -

+

+ Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +

diff --git a/docs/timeline.html b/docs/timeline.html index 5c4d0424..ae3d1370 100644 --- a/docs/timeline.html +++ b/docs/timeline.html @@ -1,12 +1,12 @@ - vis.js | timeline documentation + vis.js | timeline documentation - - + + - + @@ -17,65 +17,65 @@

Contents

Overview

- The Timeline is an interactive visualization chart to visualize data in time. - The data items can take place on a single date, or have a start and end date (a range). - You can freely move and zoom in the timeline by dragging and scrolling in the - Timeline. Items can be created, edited, and deleted in the timeline. - The time scale on the axis is adjusted automatically, and supports scales ranging - from milliseconds to years. + The Timeline is an interactive visualization chart to visualize data in time. + The data items can take place on a single date, or have a start and end date (a range). + You can freely move and zoom in the timeline by dragging and scrolling in the + Timeline. Items can be created, edited, and deleted in the timeline. + The time scale on the axis is adjusted automatically, and supports scales ranging + from milliseconds to years.

Example

- The following code shows how to create a Timeline and provide it with data. - More examples can be found in the examples directory. + The following code shows how to create a Timeline and provide it with data. + More examples can be found in the examples directory.

<!DOCTYPE HTML>
 <html>
 <head>
-    <title>Timeline | Basic demo</title>
+  <title>Timeline | Basic demo</title>
 
-    <style type="text/css">
-        body, html {
-            font-family: sans-serif;
-        }
-    </style>
+  <style type="text/css">
+    body, html {
+      font-family: sans-serif;
+    }
+  </style>
 
-    <script src="../../vis.js"></script>
+  <script src="../../vis.js"></script>
 </head>
 <body>
 <div id="visualization"></div>
 
 <script type="text/javascript">
-    var container = document.getElementById('visualization');
-    var items = [
-        {id: 1, content: 'item 1', start: '2013-04-20'},
-        {id: 2, content: 'item 2', start: '2013-04-14'},
-        {id: 3, content: 'item 3', start: '2013-04-18'},
-        {id: 4, content: 'item 4', start: '2013-04-16', end: '2013-04-19'},
-        {id: 5, content: 'item 5', start: '2013-04-25'},
-        {id: 6, content: 'item 6', start: '2013-04-27'}
-    ];
-    var options = {};
-    var timeline = new vis.Timeline(container, items, options);
+  var container = document.getElementById('visualization');
+  var items = [
+    {id: 1, content: 'item 1', start: '2013-04-20'},
+    {id: 2, content: 'item 2', start: '2013-04-14'},
+    {id: 3, content: 'item 3', start: '2013-04-18'},
+    {id: 4, content: 'item 4', start: '2013-04-16', end: '2013-04-19'},
+    {id: 5, content: 'item 5', start: '2013-04-25'},
+    {id: 6, content: 'item 6', start: '2013-04-27'}
+  ];
+  var options = {};
+  var timeline = new vis.Timeline(container, items, options);
 </script>
 </body>
 </html>
@@ -84,8 +84,8 @@
 
 

Loading

- Install or download the vis.js library. - in a subfolder of your project. Include the library script in the head of your html code: + Install or download the vis.js library. + in a subfolder of your project. Include the library script in the head of your html code:

@@ -98,199 +98,199 @@ The constructor of the Timeline is vis.Timeline
 
 The constructor accepts three parameters:
 
    -
  • - container is the DOM element in which to create the graph. -
  • -
  • - items is an Array containing items. The properties of an - item are described in section Data Format. -
  • -
  • - options is an optional Object containing a name-value map - with options. Options can also be set using the method - setOptions. -
  • +
  • + container is the DOM element in which to create the graph. +
  • +
  • + items is an Array containing items. The properties of an + item are described in section Data Format. +
  • +
  • + options is an optional Object containing a name-value map + with options. Options can also be set using the method + setOptions. +

Data Format

- The timeline can be provided with two types of data: + The timeline can be provided with two types of data:

    -
  • Items containing a set of items to be displayed in time.
  • -
  • Groups containing a set of groups used to group items +
  • Items containing a set of items to be displayed in time.
  • +
  • Groups containing a set of groups used to group items together.

Items

- The Timeline uses regular Arrays and Objects as data format. - Data items can contain the properties start, - end (optional), content, - group (optional), and className (optional). + The Timeline uses regular Arrays and Objects as data format. + Data items can contain the properties start, + end (optional), content, + group (optional), and className (optional).

- A table is constructed as: + A table is constructed as:

 var items = [
-    {
-        start: new Date(2010, 7, 15),
-        end: new Date(2010, 8, 2),  // end is optional
-        content: 'Trajectory A'
-        // Optional: fields 'id', 'type', 'group', 'className'
-    }
-    // more items...
+  {
+    start: new Date(2010, 7, 15),
+    end: new Date(2010, 8, 2),  // end is optional
+    content: 'Trajectory A'
+    // Optional: fields 'id', 'type', 'group', 'className'
+  }
+  // more items...
 ]);
 

- The item properties are defined as: + The item properties are defined as:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeRequiredDescription
idString | NumbernoAn id for the item. Using an id is not required but highly - recommended. An id is needed when dynamically adding, updating, - and removing items in a DataSet.
startDateyesThe start date of the item, for example new Date(2010,09,23).
endDatenoThe end date of the item. The end date is optional, and can be left null. - If end date is provided, the item is displayed as a range. - If not, the item is displayed as a box.
contentStringyesThe contents of the item. This can be plain text or html code.
typeString'box'The type of the item. Can be 'box' (default), 'range', or 'point'. - -
groupany typenoThis field is optional. When the group column is provided, - all items with the same group are placed on one line. - A vertical axis is displayed showing the groups. - Grouping items can be useful for example when showing availability of multiple - people, rooms, or other resources next to each other.
-
classNameStringnoThis field is optional. A className can be used to give items - an individual css style. For example, when an item has className - 'red', one can define a css style - - .red { - background-color: red; - border-color: dark-red; - } - . - More details on how to style items can be found in the section - Styles. -
NameTypeRequiredDescription
idString | NumbernoAn id for the item. Using an id is not required but highly + recommended. An id is needed when dynamically adding, updating, + and removing items in a DataSet.
startDateyesThe start date of the item, for example new Date(2010,09,23).
endDatenoThe end date of the item. The end date is optional, and can be left null. + If end date is provided, the item is displayed as a range. + If not, the item is displayed as a box.
contentStringyesThe contents of the item. This can be plain text or html code.
typeString'box'The type of the item. Can be 'box' (default), 'range', or 'point'. + +
groupany typenoThis field is optional. When the group column is provided, + all items with the same group are placed on one line. + A vertical axis is displayed showing the groups. + Grouping items can be useful for example when showing availability of multiple + people, rooms, or other resources next to each other.
+
classNameStringnoThis field is optional. A className can be used to give items + an individual css style. For example, when an item has className + 'red', one can define a css style + + .red { + background-color: red; + border-color: dark-red; + } + . + More details on how to style items can be found in the section + Styles. +

Groups

- Like the items, groups are regular JavaScript Arrays and Objects. - Using groups, items can be grouped together. - Items are filtered per group, and displayed as + Like the items, groups are regular JavaScript Arrays and Objects. + Using groups, items can be grouped together. + Items are filtered per group, and displayed as - Group items can contain the properties id, - content, and className (optional). + Group items can contain the properties id, + content, and className (optional).

- Groups can be applied to a timeline using the method setGroups. - A table with groups can be created like: + Groups can be applied to a timeline using the method setGroups. + A table with groups can be created like:

 var groups = [
-    {
-        id: 1,
-        content: 'Group 1'
-        // Optional: a field 'className'
-    }
-    // more groups...
+  {
+    id: 1,
+    content: 'Group 1'
+    // Optional: a field 'className'
+  }
+  // more groups...
 ]);
 

- Groups can have the following properties: + Groups can have the following properties:

- - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeRequiredDescription
idString | NumberyesAn id for the group. The group will display all items having a - property group which matches the id - of the group.
contentStringyesThe contents of the group. This can be plain text or html code.
classNameStringnoThis field is optional. A className can be used to give groups - an individual css style. For example, when a group has className - 'red', one can define a css style - - .red { - color: red; - } - . - More details on how to style groups can be found in the section - Styles. -
NameTypeRequiredDescription
idString | NumberyesAn id for the group. The group will display all items having a + property group which matches the id + of the group.
contentStringyesThe contents of the group. This can be plain text or html code.
classNameStringnoThis field is optional. A className can be used to give groups + an individual css style. For example, when a group has className + 'red', one can define a css style + + .red { + color: red; + } + . + More details on how to style groups can be found in the section + Styles. +
@@ -298,282 +298,282 @@ var groups = [

Configuration Options

- Options can be used to customize the timeline. - Options are defined as a JSON object. All options are optional. + Options can be used to customize the timeline. + Options are defined as a JSON object. All options are optional.

 var options = {
-    width: '100%',
-    height: '30px'
+  width: '100%',
+  height: '30px'
 };
 

- The following options are available. + The following options are available.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDefaultDescription
alignString"center"Alignment of items with type 'box'. Available values are - 'center' (default), 'left', or 'right').
autoResizebooleanfalseIf true, the Timeline will automatically detect when its - container is resized, and redraw itself accordingly.
endDatenoneThe initial end date for the axis of the timeline. - If not provided, the latest date present in the items set is taken as - end date.
groupOrderString | FunctionnoneOrder the groups by a field name or custom sort function. - By default, groups are not ordered. -
heightStringnoneThe height of the timeline in pixels or as a percentage. - When height is undefined or null, the height of the timeline is automatically - adjusted to fit the contents. - It is possible to set a maximum height using option maxHeight - to prevent the timeline from getting too high in case of automatically - calculated height. -
margin.axisNumber20The minimal margin in pixels between items and the time axis.
margin.itemNumber10The minimal margin in pixels between items.
maxDatenoneSet a maximum Date for the visible range. - It will not be possible to move beyond this maximum. -
maxHeightNumbernoneSpecifies a maximum height for the Timeline in pixels. -
minDatenoneSet a minimum Date for the visible range. - It will not be possible to move beyond this minimum. -
orderFunctionnoneProvide a custom sort function to order the items. The order of the - items is determining the way they are stacked. The function - order is called with two parameters, both of type - `vis.components.items.Item`. -
orientationString'bottom'Orientation of the timeline: 'top' or 'bottom' (default). - If orientation is 'bottom', the time axis is drawn at the bottom, - and if 'top', the axis is drawn on top.
paddingNumber5The padding of items, needed to correctly calculate the size - of item ranges. Must correspond with the css of item ranges.
showCurrentTimebooleanfalseShow a vertical bar at the current time.
showCustomTimebooleanfalseShow a vertical bar displaying a custom time. This line can be dragged by the user. The custom time can be utilized to show a state in the past or in the future. -
showMajorLabelsbooleantrueBy default, the timeline shows both minor and major date labels on the - time axis. - For example the minor labels show minutes and the major labels show hours. - When showMajorLabels is false, no major labels - are shown.
showMinorLabelsbooleantrueBy default, the timeline shows both minor and major date labels on the - time axis. - For example the minor labels show minutes and the major labels show hours. - When showMinorLabels is false, no minor labels - are shown. When both showMajorLabels and - showMinorLabels are false, no horizontal axis will be - visible.
startDatenoneThe initial start date for the axis of the timeline. - If not provided, the earliest date present in the events is taken as start date.
typeString'box'Specifies the type for the timeline items. Choose from 'dot' or 'point'. - Note that individual items can override this global type. -
widthString'100%'The width of the timeline in pixels or as a percentage.
zoomMaxNumber315360000000000Set a maximum zoom interval for the visible range in milliseconds. - It will not be possible to zoom out further than this maximum. - Default value equals about 10000 years. -
zoomMinNumber10Set a minimum zoom interval for the visible range in milliseconds. - It will not be possible to zoom in further than this minimum. -
NameTypeDefaultDescription
alignString"center"Alignment of items with type 'box'. Available values are + 'center' (default), 'left', or 'right').
autoResizebooleanfalseIf true, the Timeline will automatically detect when its + container is resized, and redraw itself accordingly.
endDatenoneThe initial end date for the axis of the timeline. + If not provided, the latest date present in the items set is taken as + end date.
groupOrderString | FunctionnoneOrder the groups by a field name or custom sort function. + By default, groups are not ordered. +
heightStringnoneThe height of the timeline in pixels or as a percentage. + When height is undefined or null, the height of the timeline is automatically + adjusted to fit the contents. + It is possible to set a maximum height using option maxHeight + to prevent the timeline from getting too high in case of automatically + calculated height. +
margin.axisNumber20The minimal margin in pixels between items and the time axis.
margin.itemNumber10The minimal margin in pixels between items.
maxDatenoneSet a maximum Date for the visible range. + It will not be possible to move beyond this maximum. +
maxHeightNumbernoneSpecifies a maximum height for the Timeline in pixels. +
minDatenoneSet a minimum Date for the visible range. + It will not be possible to move beyond this minimum. +
orderFunctionnoneProvide a custom sort function to order the items. The order of the + items is determining the way they are stacked. The function + order is called with two parameters, both of type + `vis.components.items.Item`. +
orientationString'bottom'Orientation of the timeline: 'top' or 'bottom' (default). + If orientation is 'bottom', the time axis is drawn at the bottom, + and if 'top', the axis is drawn on top.
paddingNumber5The padding of items, needed to correctly calculate the size + of item ranges. Must correspond with the css of item ranges.
showCurrentTimebooleanfalseShow a vertical bar at the current time.
showCustomTimebooleanfalseShow a vertical bar displaying a custom time. This line can be dragged by the user. The custom time can be utilized to show a state in the past or in the future. +
showMajorLabelsbooleantrueBy default, the timeline shows both minor and major date labels on the + time axis. + For example the minor labels show minutes and the major labels show hours. + When showMajorLabels is false, no major labels + are shown.
showMinorLabelsbooleantrueBy default, the timeline shows both minor and major date labels on the + time axis. + For example the minor labels show minutes and the major labels show hours. + When showMinorLabels is false, no minor labels + are shown. When both showMajorLabels and + showMinorLabels are false, no horizontal axis will be + visible.
startDatenoneThe initial start date for the axis of the timeline. + If not provided, the earliest date present in the events is taken as start date.
typeString'box'Specifies the type for the timeline items. Choose from 'dot' or 'point'. + Note that individual items can override this global type. +
widthString'100%'The width of the timeline in pixels or as a percentage.
zoomMaxNumber315360000000000Set a maximum zoom interval for the visible range in milliseconds. + It will not be possible to zoom out further than this maximum. + Default value equals about 10000 years. +
zoomMinNumber10Set a minimum zoom interval for the visible range in milliseconds. + It will not be possible to zoom in further than this minimum. +

Methods

- The Timeline supports the following methods. + The Timeline supports the following methods.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodReturn TypeDescription
getCustomTime()DateRetrieve the custom time. Only applicable when the option showCustomTime is true. -
setCustomTime(time)noneAdjust the custom time bar. Only applicable when the option showCustomTime is true. time is a Date object. -
setGroups(groups)noneSet a data set with groups for the Timeline. - groups can be an Array with Objects, - a DataSet, or a DataView. For each of the groups, the items of the - timeline are filtered on the property group, which - must correspond with the id of the group. -
setItems(items)noneSet a data set with items for the Timeline. - items can be an Array with Objects, - a DataSet, or a DataView. -
setOptions(options)noneSet or update options. It is possible to change any option - of the timeline at any time. You can for example switch orientation - on the fly. -
MethodReturn TypeDescription
getCustomTime()DateRetrieve the custom time. Only applicable when the option showCustomTime is true. +
setCustomTime(time)noneAdjust the custom time bar. Only applicable when the option showCustomTime is true. time is a Date object. +
setGroups(groups)noneSet a data set with groups for the Timeline. + groups can be an Array with Objects, + a DataSet, or a DataView. For each of the groups, the items of the + timeline are filtered on the property group, which + must correspond with the id of the group. +
setItems(items)noneSet a data set with items for the Timeline. + items can be an Array with Objects, + a DataSet, or a DataView. +
setOptions(options)noneSet or update options. It is possible to change any option + of the timeline at any time. You can for example switch orientation + on the fly. +
@@ -581,26 +581,26 @@ var options = {

Styles

- All parts of the Timeline have a class name and a default css style. - The styles can be overwritten, which enables full customization of the layout - of the Timeline. + All parts of the Timeline have a class name and a default css style. + The styles can be overwritten, which enables full customization of the layout + of the Timeline.

For example, to change the border and background color of all items, include the - following code inside the head of your html code or in a separate stylesheet.

+ following code inside the head of your html code or in a separate stylesheet.

<style>
-    .graph .item {
-      border-color: orange;
-      background-color: yellow;
-    }
+  .graph .item {
+    border-color: orange;
+    background-color: yellow;
+  }
 </style>
 

Data Policy

- All code and data is processed and rendered in the browser. - No data is sent to any server. + All code and data is processed and rendered in the browser. + No data is sent to any server.

diff --git a/examples/graph/01_basic_usage.html b/examples/graph/01_basic_usage.html index 3bed9c75..0698b9cb 100644 --- a/examples/graph/01_basic_usage.html +++ b/examples/graph/01_basic_usage.html @@ -1,17 +1,17 @@ - Graph | Basic usage + Graph | Basic usage - + - + @@ -19,31 +19,31 @@
diff --git a/examples/graph/02_random_nodes.html b/examples/graph/02_random_nodes.html index b048a69d..3d4b5a96 100755 --- a/examples/graph/02_random_nodes.html +++ b/examples/graph/02_random_nodes.html @@ -1,105 +1,105 @@ - Graph | Random nodes + Graph | Random nodes - + - + - + // add event listeners + vis.events.addListener(graph, 'select', function(params) { + document.getElementById('selection').innerHTML = + 'Selection: ' + graph.getSelection(); + }); + } +
- - - + + +

diff --git a/examples/graph/03_images.html b/examples/graph/03_images.html index f0235fb6..74e080cb 100755 --- a/examples/graph/03_images.html +++ b/examples/graph/03_images.html @@ -1,78 +1,78 @@ - Graph | Images - - - - - - + Graph | Images + + + + + + diff --git a/examples/graph/04_shapes.html b/examples/graph/04_shapes.html index 25b3f3ea..a13c8070 100755 --- a/examples/graph/04_shapes.html +++ b/examples/graph/04_shapes.html @@ -1,71 +1,71 @@ - Graph | Shapes + Graph | Shapes - + - + - + } + + // create a graph + var container = document.getElementById('mygraph'); + var data = { + nodes: nodes, + edges: edges + }; + var options = { + stabilize: false + }; + graph = new vis.Graph(container, data, options); + } + diff --git a/examples/graph/05_social_network.html b/examples/graph/05_social_network.html index 480eca6b..327145db 100644 --- a/examples/graph/05_social_network.html +++ b/examples/graph/05_social_network.html @@ -1,76 +1,76 @@ - Graph | Social Network + Graph | Social Network - + - + - + // create a graph + var container = document.getElementById('mygraph'); + var data = { + nodes: nodes, + edges: edges + }; + var options = {}; + graph = new vis.Graph(container, data, options); + } +

- Icons: Scrap Icons by Deleket + Icons: Scrap Icons by Deleket

diff --git a/examples/graph/06_groups.html b/examples/graph/06_groups.html index 59adfb51..f40b8e6a 100644 --- a/examples/graph/06_groups.html +++ b/examples/graph/06_groups.html @@ -1,156 +1,156 @@ - Graph | Groups + Graph | Groups + + + + + + + - - - + }; + graph = new vis.Graph(container, data, options); + } +
- Number of groups: - - Number of nodes per group: - - + Number of groups: + + Number of nodes per group: + +

diff --git a/examples/graph/07_selections.html b/examples/graph/07_selections.html index 421c3794..0cb31796 100644 --- a/examples/graph/07_selections.html +++ b/examples/graph/07_selections.html @@ -1,17 +1,17 @@ - Graph | Selections + Graph | Selections - + - + @@ -20,45 +20,45 @@
diff --git a/examples/graph/08_mobile_friendly.html b/examples/graph/08_mobile_friendly.html index de44f4a8..6601bee9 100755 --- a/examples/graph/08_mobile_friendly.html +++ b/examples/graph/08_mobile_friendly.html @@ -1,105 +1,105 @@ - Graph | Mobile friendly + Graph | Mobile friendly - + #mygraph { + width: 100%; + height: 100%; + } + - - + + - + - + }; + graph = new vis.Graph(container, data, options); + } + diff --git a/examples/graph/09_sizing.html b/examples/graph/09_sizing.html index 67a48f8e..42ed7273 100644 --- a/examples/graph/09_sizing.html +++ b/examples/graph/09_sizing.html @@ -1,77 +1,77 @@ - Graph | Sizing + Graph | Sizing - + - + - + }; + graph = new vis.Graph(container, data, options); + } + diff --git a/examples/graph/10_multiline_text.html b/examples/graph/10_multiline_text.html index 695d5fba..d7e2d1dd 100755 --- a/examples/graph/10_multiline_text.html +++ b/examples/graph/10_multiline_text.html @@ -1,47 +1,47 @@ - Graph | Multiline text + Graph | Multiline text - + - + - + // create a graph + var container = document.getElementById('mygraph'); + var data = { + nodes: nodes, + edges: edges + }; + var options = {}; + var graph = new vis.Graph(container, data, options); + } + diff --git a/examples/graph/11_custom_style.html b/examples/graph/11_custom_style.html index 20e9dd59..99225208 100644 --- a/examples/graph/11_custom_style.html +++ b/examples/graph/11_custom_style.html @@ -1,128 +1,128 @@ - Graph | Custom style + Graph | Custom style - + - + - + }; + + // create the graph + var container = document.getElementById('mygraph'); + var data = { + nodes: nodes, + edges: edges + }; + graph = new vis.Graph(container, data, options); + } + diff --git a/examples/graph/12_scalable_images.html b/examples/graph/12_scalable_images.html index 8a3da963..2a87ac62 100644 --- a/examples/graph/12_scalable_images.html +++ b/examples/graph/12_scalable_images.html @@ -1,80 +1,80 @@ - Graph | Scalable images + Graph | Scalable images - + - + - + }; + graph = new vis.Graph(container, data, options); + } + diff --git a/examples/graph/13_dashed_lines.html b/examples/graph/13_dashed_lines.html index 6f954672..3fdc2f5f 100644 --- a/examples/graph/13_dashed_lines.html +++ b/examples/graph/13_dashed_lines.html @@ -1,65 +1,65 @@ - Graph | Dashed lines + Graph | Dashed lines - + - + - + // create the graph + var container = document.getElementById('mygraph'); + var data = { + nodes: nodes, + edges: edges + }; + var options = { + nodes: { + shape: 'box' + }, + edges: { + length: 180 + }, + stabilize: false + }; + var graph = new vis.Graph(container, data, options); + } + -

- This example shows the different options for dashed lines. -

+

+ This example shows the different options for dashed lines. +

-
+
diff --git a/examples/graph/14_dot_language.html b/examples/graph/14_dot_language.html index 7d86df95..241f4403 100644 --- a/examples/graph/14_dot_language.html +++ b/examples/graph/14_dot_language.html @@ -1,18 +1,18 @@ - Graph | DOT Language + Graph | DOT Language - + -
+
- + diff --git a/examples/graph/15_dot_language_playground.html b/examples/graph/15_dot_language_playground.html index bf757cd8..dc5c6931 100644 --- a/examples/graph/15_dot_language_playground.html +++ b/examples/graph/15_dot_language_playground.html @@ -1,201 +1,201 @@ - Graph | DOT language playground - - - - + textarea.example { + display: none; + } + - - - - - - - - - + + + + + + + + +
-

DOT language playground

- -
-
- - -
-
- - -
-
+

DOT language playground

+ +
+
+ + +
+
+ + +
+
diff --git a/examples/graph/16_dynamic_data.html b/examples/graph/16_dynamic_data.html index b4ccb596..ea3040de 100644 --- a/examples/graph/16_dynamic_data.html +++ b/examples/graph/16_dynamic_data.html @@ -1,264 +1,264 @@ - Graph | DataSet + Graph | DataSet - + #graph { + width: 100%; + height: 400px; + border: 1px solid lightgray; + } + - - + + - + // create a graph + var container = $('#graph').get(0); + var data = { + nodes: nodes, + edges: edges + }; + var options = {}; + graph = new vis.Graph(container, data, options); + }); +

- This example demonstrates dynamically adding, updating and removing nodes - and edges using a DataSet. + This example demonstrates dynamically adding, updating and removing nodes + and edges using a DataSet.

Adjust

- - - - + + + +
-

Node

- - - - - - - - - - - - - - - - -
Action - - - -
-
-

Edge

- - - - - - - - - - - - - - - - - - - - - -
Action - - - -
-
+

Node

+ + + + + + + + + + + + + + + + +
Action + + + +
+
+

Edge

+ + + + + + + + + + + + + + + + + + + + + +
Action + + + +
+

View

- - - - - - - + + + + + + + - + - - + +
-

Nodes

-

-        
+

Nodes

+

+    
-

Edges

-

-        
+

Edges

+

+    
-

Graph

-
-
+

Graph

+
+
diff --git a/examples/graph/17_network_info.html b/examples/graph/17_network_info.html index 62cd9eef..ab73f517 100644 --- a/examples/graph/17_network_info.html +++ b/examples/graph/17_network_info.html @@ -1,149 +1,149 @@ - Graph | Images - - + + + + - - + }; + graph = new vis.Graph(container, data, options); + } + diff --git a/examples/graph/graphviz/graphviz_gallery.html b/examples/graph/graphviz/graphviz_gallery.html index 3260187d..6e17feb2 100644 --- a/examples/graph/graphviz/graphviz_gallery.html +++ b/examples/graph/graphviz/graphviz_gallery.html @@ -1,86 +1,86 @@ - Graph | Graphviz Gallery + Graph | Graphviz Gallery - - + + - +

- The following examples are unmodified copies from the - Graphviz Gallery. + The following examples are unmodified copies from the + Graphviz Gallery.

- Note that some style attributes of Graphviz are not supported by vis.js, - and that vis.js offers options not supported by Graphviz (which could make - some examples look much nicer). + Note that some style attributes of Graphviz are not supported by vis.js, + and that vis.js offers options not supported by Graphviz (which could make + some examples look much nicer).

- - + +

diff --git a/examples/graph/index.html b/examples/graph/index.html index 0fd44cfb..7cb11657 100644 --- a/examples/graph/index.html +++ b/examples/graph/index.html @@ -2,34 +2,34 @@ - vis.js | graph examples + vis.js | graph examples - + diff --git a/package.json b/package.json index 61bfcefb..c5954b62 100644 --- a/package.json +++ b/package.json @@ -1,34 +1,34 @@ { - "name": "vis", - "version": "0.3.0-SNAPSHOT", - "description": "A dynamic, browser-based visualization library.", - "homepage": "http://visjs.org/", - "repository": { - "type": "git", - "url": "git://github.com/almende/vis.git" - }, - "keywords": [ - "vis", - "visualization", - "web based", - "browser based", - "javascript", - "chart", - "linechart", - "timeline", - "graph", - "network", - "browser" - ], - "scripts": { - "test": "jake test --trace" - }, - "dependencies": {}, - "devDependencies": { - "jake": "latest", - "jake-utils": "latest", - "browserify": "latest", - "moment": "latest", - "hammerjs": "latest" - } + "name": "vis", + "version": "0.3.0-SNAPSHOT", + "description": "A dynamic, browser-based visualization library.", + "homepage": "http://visjs.org/", + "repository": { + "type": "git", + "url": "git://github.com/almende/vis.git" + }, + "keywords": [ + "vis", + "visualization", + "web based", + "browser based", + "javascript", + "chart", + "linechart", + "timeline", + "graph", + "network", + "browser" + ], + "scripts": { + "test": "jake test --trace" + }, + "dependencies": {}, + "devDependencies": { + "jake": "latest", + "jake-utils": "latest", + "browserify": "latest", + "moment": "latest", + "hammerjs": "latest" + } } diff --git a/src/DataSet.js b/src/DataSet.js index fb274a44..36604a58 100644 --- a/src/DataSet.js +++ b/src/DataSet.js @@ -36,31 +36,31 @@ */ // TODO: add a DataSet constructor DataSet(data, options) function DataSet (options) { - this.id = util.randomUUID(); - - this.options = options || {}; - this.data = {}; // map with data indexed by id - this.fieldId = this.options.fieldId || 'id'; // name of the field containing id - this.convert = {}; // field types by field name - - if (this.options.convert) { - for (var field in this.options.convert) { - if (this.options.convert.hasOwnProperty(field)) { - var value = this.options.convert[field]; - if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') { - this.convert[field] = 'Date'; - } - else { - this.convert[field] = value; - } - } + this.id = util.randomUUID(); + + this.options = options || {}; + this.data = {}; // map with data indexed by id + this.fieldId = this.options.fieldId || 'id'; // name of the field containing id + this.convert = {}; // field types by field name + + if (this.options.convert) { + for (var field in this.options.convert) { + if (this.options.convert.hasOwnProperty(field)) { + var value = this.options.convert[field]; + if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') { + this.convert[field] = 'Date'; } + else { + this.convert[field] = value; + } + } } + } - // event subscribers - this.subscribers = {}; + // event subscribers + this.subscribers = {}; - this.internalIds = {}; // internally generated id's + this.internalIds = {}; // internally generated id's } /** @@ -73,15 +73,15 @@ function DataSet (options) { * {String | Number} senderId */ DataSet.prototype.subscribe = function (event, callback) { - var subscribers = this.subscribers[event]; - if (!subscribers) { - subscribers = []; - this.subscribers[event] = subscribers; - } - - subscribers.push({ - callback: callback - }); + var subscribers = this.subscribers[event]; + if (!subscribers) { + subscribers = []; + this.subscribers[event] = subscribers; + } + + subscribers.push({ + callback: callback + }); }; /** @@ -90,12 +90,12 @@ DataSet.prototype.subscribe = function (event, callback) { * @param {function} callback */ DataSet.prototype.unsubscribe = function (event, callback) { - var subscribers = this.subscribers[event]; - if (subscribers) { - this.subscribers[event] = subscribers.filter(function (listener) { - return (listener.callback != callback); - }); - } + var subscribers = this.subscribers[event]; + if (subscribers) { + this.subscribers[event] = subscribers.filter(function (listener) { + return (listener.callback != callback); + }); + } }; /** @@ -106,24 +106,24 @@ DataSet.prototype.unsubscribe = function (event, callback) { * @private */ DataSet.prototype._trigger = function (event, params, senderId) { - if (event == '*') { - throw new Error('Cannot trigger event *'); - } - - var subscribers = []; - if (event in this.subscribers) { - subscribers = subscribers.concat(this.subscribers[event]); - } - if ('*' in this.subscribers) { - subscribers = subscribers.concat(this.subscribers['*']); - } - - for (var i = 0; i < subscribers.length; i++) { - var subscriber = subscribers[i]; - if (subscriber.callback) { - subscriber.callback(event, params, senderId || null); - } - } + if (event == '*') { + throw new Error('Cannot trigger event *'); + } + + var subscribers = []; + if (event in this.subscribers) { + subscribers = subscribers.concat(this.subscribers[event]); + } + if ('*' in this.subscribers) { + subscribers = subscribers.concat(this.subscribers['*']); + } + + for (var i = 0; i < subscribers.length; i++) { + var subscriber = subscribers[i]; + if (subscriber.callback) { + subscriber.callback(event, params, senderId || null); + } + } }; /** @@ -134,45 +134,45 @@ DataSet.prototype._trigger = function (event, params, senderId) { * @return {Array} addedIds Array with the ids of the added items */ DataSet.prototype.add = function (data, senderId) { - var addedIds = [], - id, - me = this; - - if (data instanceof Array) { - // Array - for (var i = 0, len = data.length; i < len; i++) { - id = me._addItem(data[i]); - addedIds.push(id); - } - } - else if (util.isDataTable(data)) { - // Google DataTable - var columns = this._getColumnNames(data); - for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) { - var item = {}; - for (var col = 0, cols = columns.length; col < cols; col++) { - var field = columns[col]; - item[field] = data.getValue(row, col); - } - - id = me._addItem(item); - addedIds.push(id); - } - } - else if (data instanceof Object) { - // Single item - id = me._addItem(data); - addedIds.push(id); - } - else { - throw new Error('Unknown dataType'); - } - - if (addedIds.length) { - this._trigger('add', {items: addedIds}, senderId); - } - - return addedIds; + var addedIds = [], + id, + me = this; + + if (data instanceof Array) { + // Array + for (var i = 0, len = data.length; i < len; i++) { + id = me._addItem(data[i]); + addedIds.push(id); + } + } + else if (util.isDataTable(data)) { + // Google DataTable + var columns = this._getColumnNames(data); + for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) { + var item = {}; + for (var col = 0, cols = columns.length; col < cols; col++) { + var field = columns[col]; + item[field] = data.getValue(row, col); + } + + id = me._addItem(item); + addedIds.push(id); + } + } + else if (data instanceof Object) { + // Single item + id = me._addItem(data); + addedIds.push(id); + } + else { + throw new Error('Unknown dataType'); + } + + if (addedIds.length) { + this._trigger('add', {items: addedIds}, senderId); + } + + return addedIds; }; /** @@ -182,60 +182,60 @@ DataSet.prototype.add = function (data, senderId) { * @return {Array} updatedIds The ids of the added or updated items */ DataSet.prototype.update = function (data, senderId) { - var addedIds = [], - updatedIds = [], - me = this, - fieldId = me.fieldId; - - var addOrUpdate = function (item) { - var id = item[fieldId]; - if (me.data[id]) { - // update item - id = me._updateItem(item); - updatedIds.push(id); - } - else { - // add new item - id = me._addItem(item); - addedIds.push(id); - } - }; - - if (data instanceof Array) { - // Array - for (var i = 0, len = data.length; i < len; i++) { - addOrUpdate(data[i]); - } - } - else if (util.isDataTable(data)) { - // Google DataTable - var columns = this._getColumnNames(data); - for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) { - var item = {}; - for (var col = 0, cols = columns.length; col < cols; col++) { - var field = columns[col]; - item[field] = data.getValue(row, col); - } - - addOrUpdate(item); - } - } - else if (data instanceof Object) { - // Single item - addOrUpdate(data); + var addedIds = [], + updatedIds = [], + me = this, + fieldId = me.fieldId; + + var addOrUpdate = function (item) { + var id = item[fieldId]; + if (me.data[id]) { + // update item + id = me._updateItem(item); + updatedIds.push(id); } else { - throw new Error('Unknown dataType'); - } - - if (addedIds.length) { - this._trigger('add', {items: addedIds}, senderId); - } - if (updatedIds.length) { - this._trigger('update', {items: updatedIds}, senderId); - } - - return addedIds.concat(updatedIds); + // add new item + id = me._addItem(item); + addedIds.push(id); + } + }; + + if (data instanceof Array) { + // Array + for (var i = 0, len = data.length; i < len; i++) { + addOrUpdate(data[i]); + } + } + else if (util.isDataTable(data)) { + // Google DataTable + var columns = this._getColumnNames(data); + for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) { + var item = {}; + for (var col = 0, cols = columns.length; col < cols; col++) { + var field = columns[col]; + item[field] = data.getValue(row, col); + } + + addOrUpdate(item); + } + } + else if (data instanceof Object) { + // Single item + addOrUpdate(data); + } + else { + throw new Error('Unknown dataType'); + } + + if (addedIds.length) { + this._trigger('add', {items: addedIds}, senderId); + } + if (updatedIds.length) { + this._trigger('update', {items: updatedIds}, senderId); + } + + return addedIds.concat(updatedIds); }; /** @@ -274,138 +274,138 @@ DataSet.prototype.update = function (data, senderId) { * @throws Error */ DataSet.prototype.get = function (args) { - var me = this; - - // parse the arguments - var id, ids, options, data; - var firstType = util.getType(arguments[0]); - if (firstType == 'String' || firstType == 'Number') { - // get(id [, options] [, data]) - id = arguments[0]; - options = arguments[1]; - data = arguments[2]; - } - else if (firstType == 'Array') { - // get(ids [, options] [, data]) - ids = arguments[0]; - options = arguments[1]; - data = arguments[2]; + var me = this; + + // parse the arguments + var id, ids, options, data; + var firstType = util.getType(arguments[0]); + if (firstType == 'String' || firstType == 'Number') { + // get(id [, options] [, data]) + id = arguments[0]; + options = arguments[1]; + data = arguments[2]; + } + else if (firstType == 'Array') { + // get(ids [, options] [, data]) + ids = arguments[0]; + options = arguments[1]; + data = arguments[2]; + } + else { + // get([, options] [, data]) + options = arguments[0]; + data = arguments[1]; + } + + // determine the return type + var type; + if (options && options.type) { + type = (options.type == 'DataTable') ? 'DataTable' : 'Array'; + + if (data && (type != util.getType(data))) { + throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' + + 'does not correspond with specified options.type (' + options.type + ')'); + } + if (type == 'DataTable' && !util.isDataTable(data)) { + throw new Error('Parameter "data" must be a DataTable ' + + 'when options.type is "DataTable"'); + } + } + else if (data) { + type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array'; + } + else { + type = 'Array'; + } + + // build options + var convert = options && options.convert || this.options.convert; + var filter = options && options.filter; + var items = [], item, itemId, i, len; + + // convert items + if (id != undefined) { + // return a single item + item = me._getItem(id, convert); + if (filter && !filter(item)) { + item = null; + } + } + else if (ids != undefined) { + // return a subset of items + for (i = 0, len = ids.length; i < len; i++) { + item = me._getItem(ids[i], convert); + if (!filter || filter(item)) { + items.push(item); + } + } + } + else { + // return all items + for (itemId in this.data) { + if (this.data.hasOwnProperty(itemId)) { + item = me._getItem(itemId, convert); + if (!filter || filter(item)) { + items.push(item); + } + } + } + } + + // order the results + if (options && options.order && id == undefined) { + this._sort(items, options.order); + } + + // filter fields of the items + if (options && options.fields) { + var fields = options.fields; + if (id != undefined) { + item = this._filterFields(item, fields); } else { - // get([, options] [, data]) - options = arguments[0]; - data = arguments[1]; + for (i = 0, len = items.length; i < len; i++) { + items[i] = this._filterFields(items[i], fields); + } } + } - // determine the return type - var type; - if (options && options.type) { - type = (options.type == 'DataTable') ? 'DataTable' : 'Array'; - - if (data && (type != util.getType(data))) { - throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' + - 'does not correspond with specified options.type (' + options.type + ')'); - } - if (type == 'DataTable' && !util.isDataTable(data)) { - throw new Error('Parameter "data" must be a DataTable ' + - 'when options.type is "DataTable"'); - } - } - else if (data) { - type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array'; + // return the results + if (type == 'DataTable') { + var columns = this._getColumnNames(data); + if (id != undefined) { + // append a single item to the data table + me._appendRow(data, columns, item); } else { - type = 'Array'; - } - - // build options - var convert = options && options.convert || this.options.convert; - var filter = options && options.filter; - var items = [], item, itemId, i, len; - - // convert items + // copy the items to the provided data table + for (i = 0, len = items.length; i < len; i++) { + me._appendRow(data, columns, items[i]); + } + } + return data; + } + else { + // return an array if (id != undefined) { - // return a single item - item = me._getItem(id, convert); - if (filter && !filter(item)) { - item = null; - } - } - else if (ids != undefined) { - // return a subset of items - for (i = 0, len = ids.length; i < len; i++) { - item = me._getItem(ids[i], convert); - if (!filter || filter(item)) { - items.push(item); - } - } + // a single item + return item; } else { - // return all items - for (itemId in this.data) { - if (this.data.hasOwnProperty(itemId)) { - item = me._getItem(itemId, convert); - if (!filter || filter(item)) { - items.push(item); - } - } - } - } - - // order the results - if (options && options.order && id == undefined) { - this._sort(items, options.order); - } - - // filter fields of the items - if (options && options.fields) { - var fields = options.fields; - if (id != undefined) { - item = this._filterFields(item, fields); - } - else { - for (i = 0, len = items.length; i < len; i++) { - items[i] = this._filterFields(items[i], fields); - } - } - } - - // return the results - if (type == 'DataTable') { - var columns = this._getColumnNames(data); - if (id != undefined) { - // append a single item to the data table - me._appendRow(data, columns, item); - } - else { - // copy the items to the provided data table - for (i = 0, len = items.length; i < len; i++) { - me._appendRow(data, columns, items[i]); - } + // multiple items + if (data) { + // copy the items to the provided array + for (i = 0, len = items.length; i < len; i++) { + data.push(items[i]); } return data; + } + else { + // just return our array + return items; + } } - else { - // return an array - if (id != undefined) { - // a single item - return item; - } - else { - // multiple items - if (data) { - // copy the items to the provided array - for (i = 0, len = items.length; i < len; i++) { - data.push(items[i]); - } - return data; - } - else { - // just return our array - return items; - } - } - } + } }; /** @@ -417,78 +417,78 @@ DataSet.prototype.get = function (args) { * @return {Array} ids */ DataSet.prototype.getIds = function (options) { - var data = this.data, - filter = options && options.filter, - order = options && options.order, - convert = options && options.convert || this.options.convert, - i, - len, - id, - item, - items, - ids = []; - - if (filter) { - // get filtered items - if (order) { - // create ordered list - items = []; - for (id in data) { - if (data.hasOwnProperty(id)) { - item = this._getItem(id, convert); - 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]; - } - } - else { - // create unordered list - for (id in data) { - if (data.hasOwnProperty(id)) { - item = this._getItem(id, convert); - if (filter(item)) { - ids.push(item[this.fieldId]); - } - } - } + var data = this.data, + filter = options && options.filter, + order = options && options.order, + convert = options && options.convert || this.options.convert, + i, + len, + id, + item, + items, + ids = []; + + if (filter) { + // get filtered items + if (order) { + // create ordered list + items = []; + for (id in data) { + if (data.hasOwnProperty(id)) { + item = this._getItem(id, convert); + 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]; + } } else { - // get all items - if (order) { - // create an ordered list - items = []; - for (id in data) { - if (data.hasOwnProperty(id)) { - items.push(data[id]); - } - } - - this._sort(items, order); - - for (i = 0, len = items.length; i < len; i++) { - ids[i] = items[i][this.fieldId]; - } + // create unordered list + for (id in data) { + if (data.hasOwnProperty(id)) { + item = this._getItem(id, convert); + if (filter(item)) { + ids.push(item[this.fieldId]); + } + } + } + } + } + else { + // get all items + if (order) { + // create an ordered list + items = []; + for (id in data) { + if (data.hasOwnProperty(id)) { + items.push(data[id]); } - else { - // create unordered list - for (id in data) { - if (data.hasOwnProperty(id)) { - item = data[id]; - ids.push(item[this.fieldId]); - } - } + } + + this._sort(items, order); + + for (i = 0, len = items.length; i < len; i++) { + ids[i] = items[i][this.fieldId]; + } + } + else { + // create unordered list + for (id in data) { + if (data.hasOwnProperty(id)) { + item = data[id]; + ids.push(item[this.fieldId]); } + } } + } - return ids; + return ids; }; /** @@ -503,33 +503,33 @@ DataSet.prototype.getIds = function (options) { * a field name or custom sort function. */ DataSet.prototype.forEach = function (callback, options) { - var filter = options && options.filter, - convert = options && options.convert || this.options.convert, - data = this.data, - item, - id; - - if (options && options.order) { - // execute forEach on ordered list - var items = this.get(options); - - for (var i = 0, len = items.length; i < len; i++) { - item = items[i]; - id = item[this.fieldId]; - callback(item, id); - } - } - else { - // unordered - for (id in data) { - if (data.hasOwnProperty(id)) { - item = this._getItem(id, convert); - if (!filter || filter(item)) { - callback(item, id); - } - } - } - } + var filter = options && options.filter, + convert = options && options.convert || this.options.convert, + data = this.data, + item, + id; + + if (options && options.order) { + // execute forEach on ordered list + var items = this.get(options); + + for (var i = 0, len = items.length; i < len; i++) { + item = items[i]; + id = item[this.fieldId]; + callback(item, id); + } + } + else { + // unordered + for (id in data) { + if (data.hasOwnProperty(id)) { + item = this._getItem(id, convert); + if (!filter || filter(item)) { + callback(item, id); + } + } + } + } }; /** @@ -544,28 +544,28 @@ DataSet.prototype.forEach = function (callback, options) { * @return {Object[]} mappedItems */ DataSet.prototype.map = function (callback, options) { - var filter = options && options.filter, - convert = options && options.convert || this.options.convert, - mappedItems = [], - data = this.data, - item; - - // convert and filter items - for (var id in data) { - if (data.hasOwnProperty(id)) { - item = this._getItem(id, convert); - if (!filter || filter(item)) { - mappedItems.push(callback(item, id)); - } - } - } - - // order items - if (options && options.order) { - this._sort(mappedItems, options.order); - } - - return mappedItems; + var filter = options && options.filter, + convert = options && options.convert || this.options.convert, + mappedItems = [], + data = this.data, + item; + + // convert and filter items + for (var id in data) { + if (data.hasOwnProperty(id)) { + item = this._getItem(id, convert); + if (!filter || filter(item)) { + mappedItems.push(callback(item, id)); + } + } + } + + // order items + if (options && options.order) { + this._sort(mappedItems, options.order); + } + + return mappedItems; }; /** @@ -576,15 +576,15 @@ DataSet.prototype.map = function (callback, options) { * @private */ DataSet.prototype._filterFields = function (item, fields) { - var filteredItem = {}; + var filteredItem = {}; - for (var field in item) { - if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) { - filteredItem[field] = item[field]; - } + for (var field in item) { + if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) { + filteredItem[field] = item[field]; } + } - return filteredItem; + return filteredItem; }; /** @@ -594,24 +594,24 @@ DataSet.prototype._filterFields = function (item, fields) { * @private */ DataSet.prototype._sort = function (items, order) { - if (util.isString(order)) { - // order by provided field name - var name = order; // field name - items.sort(function (a, b) { - var av = a[name]; - var bv = b[name]; - return (av > bv) ? 1 : ((av < bv) ? -1 : 0); - }); - } - else if (typeof order === 'function') { - // order by sort function - items.sort(order); - } - // TODO: extend order by an Object {field:String, direction:String} - // where direction can be 'asc' or 'desc' - else { - throw new TypeError('Order must be a function or a string'); - } + if (util.isString(order)) { + // order by provided field name + var name = order; // field name + items.sort(function (a, b) { + var av = a[name]; + var bv = b[name]; + return (av > bv) ? 1 : ((av < bv) ? -1 : 0); + }); + } + else if (typeof order === 'function') { + // order by sort function + items.sort(order); + } + // TODO: extend order by an Object {field:String, direction:String} + // where direction can be 'asc' or 'desc' + else { + throw new TypeError('Order must be a function or a string'); + } }; /** @@ -622,29 +622,29 @@ DataSet.prototype._sort = function (items, order) { * @return {Array} removedIds */ DataSet.prototype.remove = function (id, senderId) { - var removedIds = [], - i, len, removedId; - - if (id instanceof Array) { - for (i = 0, len = id.length; i < len; i++) { - removedId = this._remove(id[i]); - if (removedId != null) { - removedIds.push(removedId); - } - } + var removedIds = [], + i, len, removedId; + + if (id instanceof Array) { + for (i = 0, len = id.length; i < len; i++) { + removedId = this._remove(id[i]); + if (removedId != null) { + removedIds.push(removedId); + } } - else { - removedId = this._remove(id); - if (removedId != null) { - removedIds.push(removedId); - } + } + else { + removedId = this._remove(id); + if (removedId != null) { + removedIds.push(removedId); } + } - if (removedIds.length) { - this._trigger('remove', {items: removedIds}, senderId); - } + if (removedIds.length) { + this._trigger('remove', {items: removedIds}, senderId); + } - return removedIds; + return removedIds; }; /** @@ -654,22 +654,22 @@ DataSet.prototype.remove = function (id, senderId) { * @private */ DataSet.prototype._remove = function (id) { - if (util.isNumber(id) || util.isString(id)) { - if (this.data[id]) { - delete this.data[id]; - delete this.internalIds[id]; - return id; - } - } - else if (id instanceof Object) { - var itemId = id[this.fieldId]; - if (itemId && this.data[itemId]) { - delete this.data[itemId]; - delete this.internalIds[itemId]; - return itemId; - } - } - return null; + if (util.isNumber(id) || util.isString(id)) { + if (this.data[id]) { + delete this.data[id]; + delete this.internalIds[id]; + return id; + } + } + else if (id instanceof Object) { + var itemId = id[this.fieldId]; + if (itemId && this.data[itemId]) { + delete this.data[itemId]; + delete this.internalIds[itemId]; + return itemId; + } + } + return null; }; /** @@ -678,14 +678,14 @@ DataSet.prototype._remove = function (id) { * @return {Array} removedIds The ids of all removed items */ DataSet.prototype.clear = function (senderId) { - var ids = Object.keys(this.data); + var ids = Object.keys(this.data); - this.data = {}; - this.internalIds = {}; + this.data = {}; + this.internalIds = {}; - this._trigger('remove', {items: ids}, senderId); + this._trigger('remove', {items: ids}, senderId); - return ids; + return ids; }; /** @@ -694,22 +694,22 @@ DataSet.prototype.clear = function (senderId) { * @return {Object | null} item Item containing max value, or null if no items */ DataSet.prototype.max = function (field) { - var data = this.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; - } - } - } - - return max; + var data = this.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; + } + } + } + + return max; }; /** @@ -718,22 +718,22 @@ DataSet.prototype.max = function (field) { * @return {Object | null} item Item containing max value, or null if no items */ DataSet.prototype.min = function (field) { - var data = this.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; - } - } - } - - return min; + var data = this.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; + } + } + } + + return min; }; /** @@ -745,30 +745,30 @@ DataSet.prototype.min = function (field) { * The returned array is unordered. */ DataSet.prototype.distinct = function (field) { - var data = this.data, - values = [], - fieldType = this.options.convert[field], - count = 0; - - for (var prop in data) { - if (data.hasOwnProperty(prop)) { - var item = data[prop]; - var value = util.convert(item[field], fieldType); - var exists = false; - for (var i = 0; i < count; i++) { - if (values[i] == value) { - exists = true; - break; - } - } - if (!exists) { - values[count] = value; - count++; - } - } - } - - return values; + var data = this.data, + values = [], + fieldType = this.options.convert[field], + count = 0; + + for (var prop in data) { + if (data.hasOwnProperty(prop)) { + var item = data[prop]; + var value = util.convert(item[field], fieldType); + var exists = false; + for (var i = 0; i < count; i++) { + if (values[i] == value) { + exists = true; + break; + } + } + if (!exists) { + values[count] = value; + count++; + } + } + } + + return values; }; /** @@ -778,32 +778,32 @@ DataSet.prototype.distinct = function (field) { * @private */ DataSet.prototype._addItem = function (item) { - var id = item[this.fieldId]; - - if (id != undefined) { - // check whether this id is already taken - if (this.data[id]) { - // item already exists - throw new Error('Cannot add item: item with id ' + id + ' already exists'); - } - } - else { - // generate an id - id = util.randomUUID(); - item[this.fieldId] = id; - this.internalIds[id] = item; - } - - var d = {}; - for (var field in item) { - if (item.hasOwnProperty(field)) { - var fieldType = this.convert[field]; // type may be undefined - d[field] = util.convert(item[field], fieldType); - } - } - this.data[id] = d; - - return id; + var id = item[this.fieldId]; + + if (id != undefined) { + // check whether this id is already taken + if (this.data[id]) { + // item already exists + throw new Error('Cannot add item: item with id ' + id + ' already exists'); + } + } + else { + // generate an id + id = util.randomUUID(); + item[this.fieldId] = id; + this.internalIds[id] = item; + } + + var d = {}; + for (var field in item) { + if (item.hasOwnProperty(field)) { + var fieldType = this.convert[field]; // type may be undefined + d[field] = util.convert(item[field], fieldType); + } + } + this.data[id] = d; + + return id; }; /** @@ -814,43 +814,43 @@ DataSet.prototype._addItem = function (item) { * @private */ DataSet.prototype._getItem = function (id, convert) { - var field, value; - - // get the item from the dataset - var raw = this.data[id]; - if (!raw) { - return null; - } - - // convert the items field types - var converted = {}, - fieldId = this.fieldId, - internalIds = this.internalIds; - if (convert) { - for (field in raw) { - if (raw.hasOwnProperty(field)) { - value = raw[field]; - // output all fields, except internal ids - if ((field != fieldId) || !(value in internalIds)) { - converted[field] = util.convert(value, convert[field]); - } - } - } - } - else { - // no field types specified, no converting needed - for (field in raw) { - if (raw.hasOwnProperty(field)) { - value = raw[field]; - // output all fields, except internal ids - if ((field != fieldId) || !(value in internalIds)) { - converted[field] = value; - } - } - } - } + var field, value; - return converted; + // get the item from the dataset + var raw = this.data[id]; + if (!raw) { + return null; + } + + // convert the items field types + var converted = {}, + fieldId = this.fieldId, + internalIds = this.internalIds; + if (convert) { + for (field in raw) { + if (raw.hasOwnProperty(field)) { + value = raw[field]; + // output all fields, except internal ids + if ((field != fieldId) || !(value in internalIds)) { + converted[field] = util.convert(value, convert[field]); + } + } + } + } + else { + // no field types specified, no converting needed + for (field in raw) { + if (raw.hasOwnProperty(field)) { + value = raw[field]; + // output all fields, except internal ids + if ((field != fieldId) || !(value in internalIds)) { + converted[field] = value; + } + } + } + } + + return converted; }; /** @@ -862,25 +862,25 @@ DataSet.prototype._getItem = function (id, convert) { * @private */ DataSet.prototype._updateItem = function (item) { - var id = item[this.fieldId]; - if (id == undefined) { - throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')'); - } - var d = this.data[id]; - if (!d) { - // item doesn't exist - throw new Error('Cannot update item: no item with id ' + id + ' found'); - } - - // merge with current item - for (var field in item) { - if (item.hasOwnProperty(field)) { - var fieldType = this.convert[field]; // type may be undefined - d[field] = util.convert(item[field], fieldType); - } - } - - return id; + var id = item[this.fieldId]; + if (id == undefined) { + throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')'); + } + var d = this.data[id]; + if (!d) { + // item doesn't exist + throw new Error('Cannot update item: no item with id ' + id + ' found'); + } + + // merge with current item + for (var field in item) { + if (item.hasOwnProperty(field)) { + var fieldType = this.convert[field]; // type may be undefined + d[field] = util.convert(item[field], fieldType); + } + } + + return id; }; /** @@ -890,11 +890,11 @@ DataSet.prototype._updateItem = function (item) { * @private */ DataSet.prototype._getColumnNames = function (dataTable) { - var columns = []; - for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) { - columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col); - } - return columns; + var columns = []; + for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) { + columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col); + } + return columns; }; /** @@ -905,10 +905,10 @@ DataSet.prototype._getColumnNames = function (dataTable) { * @private */ DataSet.prototype._appendRow = function (dataTable, columns, item) { - var row = dataTable.addRow(); + var row = dataTable.addRow(); - for (var col = 0, cols = columns.length; col < cols; col++) { - var field = columns[col]; - dataTable.setValue(row, col, item[field]); - } + for (var col = 0, cols = columns.length; col < cols; col++) { + var field = columns[col]; + dataTable.setValue(row, col, item[field]); + } }; diff --git a/src/DataView.js b/src/DataView.js index 540fef17..d02ffdac 100644 --- a/src/DataView.js +++ b/src/DataView.js @@ -9,67 +9,70 @@ * @constructor DataView */ function DataView (data, options) { - this.id = util.randomUUID(); + this.id = util.randomUUID(); - this.data = null; - this.ids = {}; // ids of the items currently in memory (just contains a boolean true) - this.options = options || {}; - this.fieldId = 'id'; // name of the field containing id - this.subscribers = {}; // event subscribers + this.data = null; + this.ids = {}; // ids of the items currently in memory (just contains a boolean true) + this.options = options || {}; + this.fieldId = 'id'; // name of the field containing id + this.subscribers = {}; // event subscribers - var me = this; - this.listener = function () { - me._onEvent.apply(me, arguments); - }; + var me = this; + this.listener = function () { + me._onEvent.apply(me, arguments); + }; - this.setData(data); + this.setData(data); } +// TODO: implement a function .config() to dynamically update things like configured filter +// and trigger changes accordingly + /** * Set a data source for the view * @param {DataSet | DataView} data */ DataView.prototype.setData = function (data) { - var ids, dataItems, i, len; + var ids, dataItems, i, len; - if (this.data) { - // unsubscribe from current dataset - if (this.data.unsubscribe) { - this.data.unsubscribe('*', this.listener); - } + if (this.data) { + // unsubscribe from current dataset + if (this.data.unsubscribe) { + this.data.unsubscribe('*', this.listener); + } - // trigger a remove of all items in memory - ids = []; - for (var id in this.ids) { - if (this.ids.hasOwnProperty(id)) { - ids.push(id); - } - } - this.ids = {}; - this._trigger('remove', {items: ids}); + // trigger a remove of all items in memory + ids = []; + for (var id in this.ids) { + if (this.ids.hasOwnProperty(id)) { + ids.push(id); + } } + this.ids = {}; + this._trigger('remove', {items: ids}); + } - this.data = data; + this.data = data; - if (this.data) { - // update fieldId - this.fieldId = this.options.fieldId || - (this.data && this.data.options && this.data.options.fieldId) || - 'id'; + if (this.data) { + // update fieldId + this.fieldId = this.options.fieldId || + (this.data && this.data.options && this.data.options.fieldId) || + 'id'; - // trigger an add of all added items - ids = this.data.getIds({filter: this.options && this.options.filter}); - for (i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - this.ids[id] = true; - } - this._trigger('add', {items: ids}); + // trigger an add of all added items + ids = this.data.getIds({filter: this.options && this.options.filter}); + for (i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + this.ids[id] = true; + } + this._trigger('add', {items: ids}); - // subscribe to new dataset - if (this.data.subscribe) { - this.data.subscribe('*', this.listener); - } + // subscribe to new dataset + if (this.data.subscribe) { + this.data.subscribe('*', this.listener); } + } }; /** @@ -107,42 +110,42 @@ DataView.prototype.setData = function (data) { * @param args */ DataView.prototype.get = function (args) { - var me = this; - - // parse the arguments - var ids, options, data; - var firstType = util.getType(arguments[0]); - if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') { - // get(id(s) [, options] [, data]) - ids = arguments[0]; // can be a single id or an array with ids - options = arguments[1]; - data = arguments[2]; - } - else { - // get([, options] [, data]) - options = arguments[0]; - data = arguments[1]; - } + var me = this; - // extend the options with the default options and provided options - var viewOptions = util.extend({}, this.options, options); + // parse the arguments + var ids, options, data; + var firstType = util.getType(arguments[0]); + if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') { + // get(id(s) [, options] [, data]) + ids = arguments[0]; // can be a single id or an array with ids + options = arguments[1]; + data = arguments[2]; + } + else { + // get([, options] [, data]) + options = arguments[0]; + data = arguments[1]; + } - // create a combined filter method when needed - if (this.options.filter && options && options.filter) { - viewOptions.filter = function (item) { - return me.options.filter(item) && options.filter(item); - } - } + // extend the options with the default options and provided options + var viewOptions = util.extend({}, this.options, options); - // build up the call to the linked data set - var getArguments = []; - if (ids != undefined) { - getArguments.push(ids); + // create a combined filter method when needed + if (this.options.filter && options && options.filter) { + viewOptions.filter = function (item) { + return me.options.filter(item) && options.filter(item); } - getArguments.push(viewOptions); - getArguments.push(data); + } + + // build up the call to the linked data set + var getArguments = []; + if (ids != undefined) { + getArguments.push(ids); + } + getArguments.push(viewOptions); + getArguments.push(data); - return this.data && this.data.get.apply(this.data, getArguments); + return this.data && this.data.get.apply(this.data, getArguments); }; /** @@ -154,36 +157,36 @@ DataView.prototype.get = function (args) { * @return {Array} ids */ DataView.prototype.getIds = function (options) { - var ids; + var ids; - if (this.data) { - var defaultFilter = this.options.filter; - var filter; + 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; + if (options && options.filter) { + if (defaultFilter) { + filter = function (item) { + return defaultFilter(item) && options.filter(item); } - - ids = this.data.getIds({ - filter: filter, - order: options && options.order - }); + } + else { + filter = options.filter; + } } else { - ids = []; + filter = defaultFilter; } - return ids; + ids = this.data.getIds({ + filter: filter, + order: options && options.order + }); + } + else { + ids = []; + } + + return ids; }; /** @@ -196,80 +199,80 @@ DataView.prototype.getIds = function (options) { * @private */ DataView.prototype._onEvent = function (event, params, senderId) { - var i, len, id, item, - ids = params && params.items, - data = this.data, - added = [], - updated = [], - removed = []; - - if (ids && data) { - switch (event) { - case 'add': - // filter the ids of the added items - for (i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - item = this.get(id); - if (item) { - this.ids[id] = true; - added.push(id); - } - } - - break; - - case 'update': - // determine the event from the views viewpoint: an updated - // item can be added, updated, or removed from this view. - for (i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - item = this.get(id); - - if (item) { - if (this.ids[id]) { - updated.push(id); - } - else { - this.ids[id] = true; - added.push(id); - } - } - else { - if (this.ids[id]) { - delete this.ids[id]; - removed.push(id); - } - else { - // nothing interesting for me :-( - } - } - } - - break; - - case 'remove': - // filter the ids of the removed items - for (i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - if (this.ids[id]) { - delete this.ids[id]; - removed.push(id); - } - } - - break; - } + var i, len, id, item, + ids = params && params.items, + data = this.data, + added = [], + updated = [], + removed = []; - if (added.length) { - this._trigger('add', {items: added}, senderId); + if (ids && data) { + switch (event) { + case 'add': + // filter the ids of the added items + for (i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + item = this.get(id); + if (item) { + this.ids[id] = true; + added.push(id); + } } - if (updated.length) { - this._trigger('update', {items: updated}, senderId); + + break; + + case 'update': + // determine the event from the views viewpoint: an updated + // item can be added, updated, or removed from this view. + for (i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + item = this.get(id); + + if (item) { + if (this.ids[id]) { + updated.push(id); + } + else { + this.ids[id] = true; + added.push(id); + } + } + else { + if (this.ids[id]) { + delete this.ids[id]; + removed.push(id); + } + else { + // nothing interesting for me :-( + } + } } - if (removed.length) { - this._trigger('remove', {items: removed}, senderId); + + break; + + case 'remove': + // filter the ids of the removed items + for (i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + if (this.ids[id]) { + delete this.ids[id]; + removed.push(id); + } } + + break; + } + + if (added.length) { + this._trigger('add', {items: added}, senderId); + } + if (updated.length) { + this._trigger('update', {items: updated}, senderId); + } + if (removed.length) { + this._trigger('remove', {items: removed}, senderId); } + } }; // copy subscription functionality from DataSet diff --git a/src/EventBus.js b/src/EventBus.js index ae761a37..017739b1 100644 --- a/src/EventBus.js +++ b/src/EventBus.js @@ -3,7 +3,7 @@ * @constructor EventBus */ function EventBus() { - this.subscriptions = []; + this.subscriptions = []; } /** @@ -16,21 +16,21 @@ function EventBus() { * @returns {String} id A subscription id */ EventBus.prototype.on = function (event, callback, target) { - var regexp = (event instanceof RegExp) ? - event : - new RegExp(event.replace('*', '\\w+')); + var regexp = (event instanceof RegExp) ? + event : + new RegExp(event.replace('*', '\\w+')); - var subscription = { - id: util.randomUUID(), - event: event, - regexp: regexp, - callback: (typeof callback === 'function') ? callback : null, - target: target - }; + var subscription = { + id: util.randomUUID(), + event: event, + regexp: regexp, + callback: (typeof callback === 'function') ? callback : null, + target: target + }; - this.subscriptions.push(subscription); + this.subscriptions.push(subscription); - return subscription.id; + return subscription.id; }; /** @@ -42,33 +42,33 @@ EventBus.prototype.on = function (event, callback, target) { * callback, and target. */ EventBus.prototype.off = function (filter) { - var i = 0; - while (i < this.subscriptions.length) { - var subscription = this.subscriptions[i]; + var i = 0; + while (i < this.subscriptions.length) { + var subscription = this.subscriptions[i]; - var match = true; - if (filter instanceof Object) { - // filter is an object. All fields must match - for (var prop in filter) { - if (filter.hasOwnProperty(prop)) { - if (filter[prop] !== subscription[prop]) { - match = false; - } - } - } - } - else { - // filter is a string, filter on id - match = (subscription.id == filter); + var match = true; + if (filter instanceof Object) { + // filter is an object. All fields must match + for (var prop in filter) { + if (filter.hasOwnProperty(prop)) { + if (filter[prop] !== subscription[prop]) { + match = false; + } } + } + } + else { + // filter is a string, filter on id + match = (subscription.id == filter); + } - if (match) { - this.subscriptions.splice(i, 1); - } - else { - i++; - } + if (match) { + this.subscriptions.splice(i, 1); + } + else { + i++; } + } }; /** @@ -78,12 +78,12 @@ EventBus.prototype.off = function (filter) { * @param {*} [source] */ EventBus.prototype.emit = function (event, data, source) { - for (var i =0; i < this.subscriptions.length; i++) { - var subscription = this.subscriptions[i]; - if (subscription.regexp.test(event)) { - if (subscription.callback) { - subscription.callback(event, data, source); - } - } + for (var i =0; i < this.subscriptions.length; i++) { + var subscription = this.subscriptions[i]; + if (subscription.regexp.test(event)) { + if (subscription.callback) { + subscription.callback(event, data, source); + } } + } }; diff --git a/src/events.js b/src/events.js index 1356be25..4676aded 100644 --- a/src/events.js +++ b/src/events.js @@ -3,114 +3,114 @@ */ // TODO: replace usage of the event listener for the EventBus var events = { - 'listeners': [], + 'listeners': [], - /** - * Find a single listener by its object - * @param {Object} object - * @return {Number} index -1 when not found - */ - 'indexOf': function (object) { - var listeners = this.listeners; - for (var i = 0, iMax = this.listeners.length; i < iMax; i++) { - var listener = listeners[i]; - if (listener && listener.object == object) { - return i; - } - } - return -1; - }, + /** + * Find a single listener by its object + * @param {Object} object + * @return {Number} index -1 when not found + */ + 'indexOf': function (object) { + var listeners = this.listeners; + for (var i = 0, iMax = this.listeners.length; i < iMax; i++) { + var listener = listeners[i]; + if (listener && listener.object == object) { + return i; + } + } + return -1; + }, - /** - * Add an event listener - * @param {Object} object - * @param {String} event The name of an event, for example 'select' - * @param {function} callback The callback method, called when the - * event takes place - */ - 'addListener': function (object, event, callback) { - var index = this.indexOf(object); - var listener = this.listeners[index]; - if (!listener) { - listener = { - 'object': object, - 'events': {} - }; - this.listeners.push(listener); - } + /** + * Add an event listener + * @param {Object} object + * @param {String} event The name of an event, for example 'select' + * @param {function} callback The callback method, called when the + * event takes place + */ + 'addListener': function (object, event, callback) { + var index = this.indexOf(object); + var listener = this.listeners[index]; + if (!listener) { + listener = { + 'object': object, + 'events': {} + }; + this.listeners.push(listener); + } - var callbacks = listener.events[event]; - if (!callbacks) { - callbacks = []; - listener.events[event] = callbacks; - } + var callbacks = listener.events[event]; + if (!callbacks) { + callbacks = []; + listener.events[event] = callbacks; + } - // add the callback if it does not yet exist - if (callbacks.indexOf(callback) == -1) { - callbacks.push(callback); - } - }, + // add the callback if it does not yet exist + if (callbacks.indexOf(callback) == -1) { + callbacks.push(callback); + } + }, - /** - * Remove an event listener - * @param {Object} object - * @param {String} event The name of an event, for example 'select' - * @param {function} callback The registered callback method - */ - 'removeListener': function (object, event, callback) { - var index = this.indexOf(object); - var listener = this.listeners[index]; - if (listener) { - var callbacks = listener.events[event]; - if (callbacks) { - index = callbacks.indexOf(callback); - if (index != -1) { - callbacks.splice(index, 1); - } + /** + * Remove an event listener + * @param {Object} object + * @param {String} event The name of an event, for example 'select' + * @param {function} callback The registered callback method + */ + 'removeListener': function (object, event, callback) { + var index = this.indexOf(object); + var listener = this.listeners[index]; + if (listener) { + var callbacks = listener.events[event]; + if (callbacks) { + index = callbacks.indexOf(callback); + if (index != -1) { + callbacks.splice(index, 1); + } - // remove the array when empty - if (callbacks.length == 0) { - delete listener.events[event]; - } - } + // remove the array when empty + if (callbacks.length == 0) { + delete listener.events[event]; + } + } - // count the number of registered events. remove listener when empty - var count = 0; - var events = listener.events; - for (var e in events) { - if (events.hasOwnProperty(e)) { - count++; - } - } - if (count == 0) { - delete this.listeners[index]; - } + // count the number of registered events. remove listener when empty + var count = 0; + var events = listener.events; + for (var e in events) { + if (events.hasOwnProperty(e)) { + count++; } - }, + } + if (count == 0) { + delete this.listeners[index]; + } + } + }, - /** - * Remove all registered event listeners - */ - 'removeAllListeners': function () { - this.listeners = []; - }, + /** + * Remove all registered event listeners + */ + 'removeAllListeners': function () { + this.listeners = []; + }, - /** - * Trigger an event. All registered event handlers will be called - * @param {Object} object - * @param {String} event - * @param {Object} properties (optional) - */ - 'trigger': function (object, event, properties) { - var index = this.indexOf(object); - var listener = this.listeners[index]; - if (listener) { - var callbacks = listener.events[event]; - if (callbacks) { - for (var i = 0, iMax = callbacks.length; i < iMax; i++) { - callbacks[i](properties); - } - } + /** + * Trigger an event. All registered event handlers will be called + * @param {Object} object + * @param {String} event + * @param {Object} properties (optional) + */ + 'trigger': function (object, event, properties) { + var index = this.indexOf(object); + var listener = this.listeners[index]; + if (listener) { + var callbacks = listener.events[event]; + if (callbacks) { + for (var i = 0, iMax = callbacks.length; i < iMax; i++) { + callbacks[i](properties); } + } } + } }; diff --git a/src/graph/Edge.js b/src/graph/Edge.js index d4f5f25b..28aaa7cc 100644 --- a/src/graph/Edge.js +++ b/src/graph/Edge.js @@ -14,40 +14,40 @@ * example for the color */ function Edge (properties, graph, constants) { - if (!graph) { - throw "No graph provided"; - } - this.graph = graph; - - // initialize constants - this.widthMin = constants.edges.widthMin; - this.widthMax = constants.edges.widthMax; - - // initialize variables - this.id = undefined; - this.fromId = undefined; - this.toId = undefined; - this.style = constants.edges.style; - this.title = undefined; - this.width = constants.edges.width; - this.value = undefined; - this.length = constants.edges.length; - - this.from = null; // a node - this.to = null; // a node - this.connected = false; - - // Added to support dashed lines - // David Jordan - // 2012-08-08 - this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength - - this.stiffness = undefined; // depends on the length of the edge - this.color = constants.edges.color; - this.widthFixed = false; - this.lengthFixed = false; - - this.setProperties(properties, constants); + if (!graph) { + throw "No graph provided"; + } + this.graph = graph; + + // initialize constants + this.widthMin = constants.edges.widthMin; + this.widthMax = constants.edges.widthMax; + + // initialize variables + this.id = undefined; + this.fromId = undefined; + this.toId = undefined; + this.style = constants.edges.style; + this.title = undefined; + this.width = constants.edges.width; + this.value = undefined; + this.length = constants.edges.length; + + this.from = null; // a node + this.to = null; // a node + this.connected = false; + + // Added to support dashed lines + // David Jordan + // 2012-08-08 + this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength + + this.stiffness = undefined; // depends on the length of the edge + this.color = constants.edges.color; + this.widthFixed = false; + this.lengthFixed = false; + + this.setProperties(properties, constants); } /** @@ -56,95 +56,95 @@ function Edge (properties, graph, constants) { * @param {Object} constants and object with default, global properties */ Edge.prototype.setProperties = function(properties, constants) { - if (!properties) { - return; - } - - if (properties.from != undefined) {this.fromId = properties.from;} - if (properties.to != undefined) {this.toId = properties.to;} - - if (properties.id != undefined) {this.id = properties.id;} - if (properties.style != undefined) {this.style = properties.style;} - if (properties.label != undefined) {this.label = properties.label;} - if (this.label) { - this.fontSize = constants.edges.fontSize; - this.fontFace = constants.edges.fontFace; - this.fontColor = constants.edges.fontColor; - if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;} - if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;} - if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;} - } - if (properties.title != undefined) {this.title = properties.title;} - if (properties.width != undefined) {this.width = properties.width;} - if (properties.value != undefined) {this.value = properties.value;} - if (properties.length != undefined) {this.length = properties.length;} - - // Added to support dashed lines - // David Jordan - // 2012-08-08 - if (properties.dash) { - if (properties.dash.length != undefined) {this.dash.length = properties.dash.length;} - if (properties.dash.gap != undefined) {this.dash.gap = properties.dash.gap;} - if (properties.dash.altLength != undefined) {this.dash.altLength = properties.dash.altLength;} - } - - if (properties.color != undefined) {this.color = properties.color;} - - // A node is connected when it has a from and to node. - this.connect(); - - this.widthFixed = this.widthFixed || (properties.width != undefined); - this.lengthFixed = this.lengthFixed || (properties.length != undefined); - this.stiffness = 1 / this.length; - - // set draw method based on style - switch (this.style) { - case 'line': this.draw = this._drawLine; break; - case 'arrow': this.draw = this._drawArrow; break; - case 'arrow-center': this.draw = this._drawArrowCenter; break; - case 'dash-line': this.draw = this._drawDashLine; break; - default: this.draw = this._drawLine; break; - } + if (!properties) { + return; + } + + if (properties.from != undefined) {this.fromId = properties.from;} + if (properties.to != undefined) {this.toId = properties.to;} + + if (properties.id != undefined) {this.id = properties.id;} + if (properties.style != undefined) {this.style = properties.style;} + if (properties.label != undefined) {this.label = properties.label;} + if (this.label) { + this.fontSize = constants.edges.fontSize; + this.fontFace = constants.edges.fontFace; + this.fontColor = constants.edges.fontColor; + if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;} + if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;} + if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;} + } + if (properties.title != undefined) {this.title = properties.title;} + if (properties.width != undefined) {this.width = properties.width;} + if (properties.value != undefined) {this.value = properties.value;} + if (properties.length != undefined) {this.length = properties.length;} + + // Added to support dashed lines + // David Jordan + // 2012-08-08 + if (properties.dash) { + if (properties.dash.length != undefined) {this.dash.length = properties.dash.length;} + if (properties.dash.gap != undefined) {this.dash.gap = properties.dash.gap;} + if (properties.dash.altLength != undefined) {this.dash.altLength = properties.dash.altLength;} + } + + if (properties.color != undefined) {this.color = properties.color;} + + // A node is connected when it has a from and to node. + this.connect(); + + this.widthFixed = this.widthFixed || (properties.width != undefined); + this.lengthFixed = this.lengthFixed || (properties.length != undefined); + this.stiffness = 1 / this.length; + + // set draw method based on style + switch (this.style) { + case 'line': this.draw = this._drawLine; break; + case 'arrow': this.draw = this._drawArrow; break; + case 'arrow-center': this.draw = this._drawArrowCenter; break; + case 'dash-line': this.draw = this._drawDashLine; break; + default: this.draw = this._drawLine; break; + } }; /** * Connect an edge to its nodes */ Edge.prototype.connect = function () { - this.disconnect(); + this.disconnect(); - this.from = this.graph.nodes[this.fromId] || null; - this.to = this.graph.nodes[this.toId] || null; - this.connected = (this.from && this.to); + this.from = this.graph.nodes[this.fromId] || null; + this.to = this.graph.nodes[this.toId] || null; + this.connected = (this.from && this.to); - if (this.connected) { - this.from.attachEdge(this); - this.to.attachEdge(this); + if (this.connected) { + this.from.attachEdge(this); + this.to.attachEdge(this); + } + else { + if (this.from) { + this.from.detachEdge(this); } - else { - if (this.from) { - this.from.detachEdge(this); - } - if (this.to) { - this.to.detachEdge(this); - } + if (this.to) { + this.to.detachEdge(this); } + } }; /** * Disconnect an edge from its nodes */ Edge.prototype.disconnect = function () { - if (this.from) { - this.from.detachEdge(this); - this.from = null; - } - if (this.to) { - this.to.detachEdge(this); - this.to = null; - } - - this.connected = false; + if (this.from) { + this.from.detachEdge(this); + this.from = null; + } + if (this.to) { + this.to.detachEdge(this); + this.to = null; + } + + this.connected = false; }; /** @@ -153,7 +153,7 @@ Edge.prototype.disconnect = function () { * has been set. */ Edge.prototype.getTitle = function() { - return this.title; + return this.title; }; @@ -162,7 +162,7 @@ Edge.prototype.getTitle = function() { * @return {Number} value */ Edge.prototype.getValue = function() { - return this.value; + return this.value; }; /** @@ -172,10 +172,10 @@ Edge.prototype.getValue = function() { * @param {Number} max */ Edge.prototype.setValueRange = function(min, max) { - if (!this.widthFixed && this.value !== undefined) { - var scale = (this.widthMax - this.widthMin) / (max - min); - this.width = (this.value - min) * scale + this.widthMin; - } + if (!this.widthFixed && this.value !== undefined) { + var scale = (this.widthMax - this.widthMin) / (max - min); + this.width = (this.value - min) * scale + this.widthMin; + } }; /** @@ -185,7 +185,7 @@ Edge.prototype.setValueRange = function(min, max) { * @param {CanvasRenderingContext2D} ctx */ Edge.prototype.draw = function(ctx) { - throw "Method draw not initialized in edge"; + throw "Method draw not initialized in edge"; }; /** @@ -194,19 +194,19 @@ Edge.prototype.draw = function(ctx) { * @return {boolean} True if location is located on the edge */ Edge.prototype.isOverlappingWith = function(obj) { - var distMax = 10; + var distMax = 10; - var xFrom = this.from.x; - var yFrom = this.from.y; - var xTo = this.to.x; - var yTo = this.to.y; - var xObj = obj.left; - var yObj = obj.top; + var xFrom = this.from.x; + var yFrom = this.from.y; + var xTo = this.to.x; + var yTo = this.to.y; + var xObj = obj.left; + var yObj = obj.top; - var dist = Edge._dist(xFrom, yFrom, xTo, yTo, xObj, yObj); + var dist = Edge._dist(xFrom, yFrom, xTo, yTo, xObj, yObj); - return (dist < distMax); + return (dist < distMax); }; @@ -218,40 +218,40 @@ Edge.prototype.isOverlappingWith = function(obj) { * @private */ Edge.prototype._drawLine = function(ctx) { - // set style - ctx.strokeStyle = this.color; - ctx.lineWidth = this._getLineWidth(); - - var point; - if (this.from != this.to) { - // draw line - this._line(ctx); - - // draw label - if (this.label) { - point = this._pointOnLine(0.5); - this._label(ctx, this.label, point.x, point.y); - } + // set style + ctx.strokeStyle = this.color; + ctx.lineWidth = this._getLineWidth(); + + var point; + if (this.from != this.to) { + // draw line + this._line(ctx); + + // draw label + if (this.label) { + point = this._pointOnLine(0.5); + this._label(ctx, this.label, point.x, point.y); + } + } + else { + var x, y; + var radius = this.length / 4; + var node = this.from; + if (!node.width) { + node.resize(ctx); + } + if (node.width > node.height) { + x = node.x + node.width / 2; + y = node.y - radius; } else { - var x, y; - var radius = this.length / 4; - var node = this.from; - if (!node.width) { - node.resize(ctx); - } - if (node.width > node.height) { - x = node.x + node.width / 2; - y = node.y - radius; - } - else { - x = node.x + radius; - y = node.y - node.height / 2; - } - this._circle(ctx, x, y, radius); - point = this._pointOnCircle(x, y, radius, 0.5); - this._label(ctx, this.label, point.x, point.y); + x = node.x + radius; + y = node.y - node.height / 2; } + this._circle(ctx, x, y, radius); + point = this._pointOnCircle(x, y, radius, 0.5); + this._label(ctx, this.label, point.x, point.y); + } }; /** @@ -261,12 +261,12 @@ Edge.prototype._drawLine = function(ctx) { * @private */ Edge.prototype._getLineWidth = function() { - if (this.from.selected || this.to.selected) { - return Math.min(this.width * 2, this.widthMax); - } - else { - return this.width; - } + if (this.from.selected || this.to.selected) { + return Math.min(this.width * 2, this.widthMax); + } + else { + return this.width; + } }; /** @@ -275,11 +275,11 @@ Edge.prototype._getLineWidth = function() { * @private */ Edge.prototype._line = function (ctx) { - // draw a straight line - ctx.beginPath(); - ctx.moveTo(this.from.x, this.from.y); - ctx.lineTo(this.to.x, this.to.y); - ctx.stroke(); + // draw a straight line + ctx.beginPath(); + ctx.moveTo(this.from.x, this.from.y); + ctx.lineTo(this.to.x, this.to.y); + ctx.stroke(); }; /** @@ -291,10 +291,10 @@ Edge.prototype._line = function (ctx) { * @private */ Edge.prototype._circle = function (ctx, x, y, radius) { - // draw a circle - ctx.beginPath(); - ctx.arc(x, y, radius, 0, 2 * Math.PI, false); - ctx.stroke(); + // draw a circle + ctx.beginPath(); + ctx.arc(x, y, radius, 0, 2 * Math.PI, false); + ctx.stroke(); }; /** @@ -306,24 +306,24 @@ Edge.prototype._circle = function (ctx, x, y, radius) { * @private */ Edge.prototype._label = function (ctx, text, x, y) { - if (text) { - // TODO: cache the calculated size - ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") + - this.fontSize + "px " + this.fontFace; - ctx.fillStyle = 'white'; - var width = ctx.measureText(text).width; - var height = this.fontSize; - var left = x - width / 2; - var top = y - height / 2; - - ctx.fillRect(left, top, width, height); - - // draw text - ctx.fillStyle = this.fontColor || "black"; - ctx.textAlign = "left"; - ctx.textBaseline = "top"; - ctx.fillText(text, left, top); - } + if (text) { + // TODO: cache the calculated size + ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") + + this.fontSize + "px " + this.fontFace; + ctx.fillStyle = 'white'; + var width = ctx.measureText(text).width; + var height = this.fontSize; + var left = x - width / 2; + var top = y - height / 2; + + ctx.fillRect(left, top, width, height); + + // draw text + ctx.fillStyle = this.fontColor || "black"; + ctx.textAlign = "left"; + ctx.textBaseline = "top"; + ctx.fillText(text, left, top); + } }; /** @@ -336,35 +336,35 @@ Edge.prototype._label = function (ctx, text, x, y) { * @private */ Edge.prototype._drawDashLine = function(ctx) { - // set style - ctx.strokeStyle = this.color; - ctx.lineWidth = this._getLineWidth(); - - // draw dashed line - ctx.beginPath(); - ctx.lineCap = 'round'; - if (this.dash.altLength != undefined) //If an alt dash value has been set add to the array this value - { - ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y, - [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]); - } - else if (this.dash.length != undefined && this.dash.gap != undefined) //If a dash and gap value has been set add to the array this value - { - ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y, - [this.dash.length,this.dash.gap]); - } - else //If all else fails draw a line - { - ctx.moveTo(this.from.x, this.from.y); - ctx.lineTo(this.to.x, this.to.y); - } - ctx.stroke(); - - // draw label - if (this.label) { - var point = this._pointOnLine(0.5); - this._label(ctx, this.label, point.x, point.y); - } + // set style + ctx.strokeStyle = this.color; + ctx.lineWidth = this._getLineWidth(); + + // draw dashed line + ctx.beginPath(); + ctx.lineCap = 'round'; + if (this.dash.altLength != undefined) //If an alt dash value has been set add to the array this value + { + ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y, + [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]); + } + else if (this.dash.length != undefined && this.dash.gap != undefined) //If a dash and gap value has been set add to the array this value + { + ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y, + [this.dash.length,this.dash.gap]); + } + else //If all else fails draw a line + { + ctx.moveTo(this.from.x, this.from.y); + ctx.lineTo(this.to.x, this.to.y); + } + ctx.stroke(); + + // draw label + if (this.label) { + var point = this._pointOnLine(0.5); + this._label(ctx, this.label, point.x, point.y); + } }; /** @@ -374,10 +374,10 @@ Edge.prototype._drawDashLine = function(ctx) { * @private */ Edge.prototype._pointOnLine = function (percentage) { - return { - x: (1 - percentage) * this.from.x + percentage * this.to.x, - y: (1 - percentage) * this.from.y + percentage * this.to.y - } + return { + x: (1 - percentage) * this.from.x + percentage * this.to.x, + y: (1 - percentage) * this.from.y + percentage * this.to.y + } }; /** @@ -390,11 +390,11 @@ Edge.prototype._pointOnLine = function (percentage) { * @private */ Edge.prototype._pointOnCircle = function (x, y, radius, percentage) { - var angle = (percentage - 3/8) * 2 * Math.PI; - return { - x: x + radius * Math.cos(angle), - y: y - radius * Math.sin(angle) - } + var angle = (percentage - 3/8) * 2 * Math.PI; + return { + x: x + radius * Math.cos(angle), + y: y - radius * Math.sin(angle) + } }; /** @@ -405,62 +405,62 @@ Edge.prototype._pointOnCircle = function (x, y, radius, percentage) { * @private */ Edge.prototype._drawArrowCenter = function(ctx) { - var point; - // set style - ctx.strokeStyle = this.color; - ctx.fillStyle = this.color; - ctx.lineWidth = this._getLineWidth(); - - if (this.from != this.to) { - // draw line - this._line(ctx); - - // draw an arrow halfway the line - var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); - var length = 10 + 5 * this.width; // TODO: make customizable? - point = this._pointOnLine(0.5); - ctx.arrow(point.x, point.y, angle, length); - ctx.fill(); - ctx.stroke(); - - // draw label - if (this.label) { - point = this._pointOnLine(0.5); - this._label(ctx, this.label, point.x, point.y); - } + var point; + // set style + ctx.strokeStyle = this.color; + ctx.fillStyle = this.color; + ctx.lineWidth = this._getLineWidth(); + + if (this.from != this.to) { + // draw line + this._line(ctx); + + // draw an arrow halfway the line + var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); + var length = 10 + 5 * this.width; // TODO: make customizable? + point = this._pointOnLine(0.5); + ctx.arrow(point.x, point.y, angle, length); + ctx.fill(); + ctx.stroke(); + + // draw label + if (this.label) { + point = this._pointOnLine(0.5); + this._label(ctx, this.label, point.x, point.y); + } + } + else { + // draw circle + var x, y; + var radius = this.length / 4; + var node = this.from; + if (!node.width) { + node.resize(ctx); + } + if (node.width > node.height) { + x = node.x + node.width / 2; + y = node.y - radius; } else { - // draw circle - var x, y; - var radius = this.length / 4; - var node = this.from; - if (!node.width) { - node.resize(ctx); - } - if (node.width > node.height) { - x = node.x + node.width / 2; - y = node.y - radius; - } - else { - x = node.x + radius; - y = node.y - node.height / 2; - } - this._circle(ctx, x, y, radius); - - // draw all arrows - var angle = 0.2 * Math.PI; - var length = 10 + 5 * this.width; // TODO: make customizable? - point = this._pointOnCircle(x, y, radius, 0.5); - ctx.arrow(point.x, point.y, angle, length); - ctx.fill(); - ctx.stroke(); - - // draw label - if (this.label) { - point = this._pointOnCircle(x, y, radius, 0.5); - this._label(ctx, this.label, point.x, point.y); - } + x = node.x + radius; + y = node.y - node.height / 2; } + this._circle(ctx, x, y, radius); + + // draw all arrows + var angle = 0.2 * Math.PI; + var length = 10 + 5 * this.width; // TODO: make customizable? + point = this._pointOnCircle(x, y, radius, 0.5); + ctx.arrow(point.x, point.y, angle, length); + ctx.fill(); + ctx.stroke(); + + // draw label + if (this.label) { + point = this._pointOnCircle(x, y, radius, 0.5); + this._label(ctx, this.label, point.x, point.y); + } + } }; @@ -473,91 +473,91 @@ Edge.prototype._drawArrowCenter = function(ctx) { * @private */ Edge.prototype._drawArrow = function(ctx) { - // set style - ctx.strokeStyle = this.color; - ctx.fillStyle = this.color; - ctx.lineWidth = this._getLineWidth(); + // set style + ctx.strokeStyle = this.color; + ctx.fillStyle = this.color; + ctx.lineWidth = this._getLineWidth(); + + // draw line + var angle, length; + if (this.from != this.to) { + // calculate length and angle of the line + angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); + var dx = (this.to.x - this.from.x); + var dy = (this.to.y - this.from.y); + var lEdge = Math.sqrt(dx * dx + dy * dy); + + var lFrom = this.from.distanceToBorder(ctx, angle + Math.PI); + var pFrom = (lEdge - lFrom) / lEdge; + var xFrom = (pFrom) * this.from.x + (1 - pFrom) * this.to.x; + var yFrom = (pFrom) * this.from.y + (1 - pFrom) * this.to.y; + + var lTo = this.to.distanceToBorder(ctx, angle); + var pTo = (lEdge - lTo) / lEdge; + var xTo = (1 - pTo) * this.from.x + pTo * this.to.x; + var yTo = (1 - pTo) * this.from.y + pTo * this.to.y; - // draw line - var angle, length; - if (this.from != this.to) { - // calculate length and angle of the line - angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); - var dx = (this.to.x - this.from.x); - var dy = (this.to.y - this.from.y); - var lEdge = Math.sqrt(dx * dx + dy * dy); - - var lFrom = this.from.distanceToBorder(ctx, angle + Math.PI); - var pFrom = (lEdge - lFrom) / lEdge; - var xFrom = (pFrom) * this.from.x + (1 - pFrom) * this.to.x; - var yFrom = (pFrom) * this.from.y + (1 - pFrom) * this.to.y; - - var lTo = this.to.distanceToBorder(ctx, angle); - var pTo = (lEdge - lTo) / lEdge; - var xTo = (1 - pTo) * this.from.x + pTo * this.to.x; - var yTo = (1 - pTo) * this.from.y + pTo * this.to.y; - - ctx.beginPath(); - ctx.moveTo(xFrom, yFrom); - ctx.lineTo(xTo, yTo); - ctx.stroke(); - - // draw arrow at the end of the line - length = 10 + 5 * this.width; // TODO: make customizable? - ctx.arrow(xTo, yTo, angle, length); - ctx.fill(); - ctx.stroke(); - - // draw label - if (this.label) { - var point = this._pointOnLine(0.5); - this._label(ctx, this.label, point.x, point.y); - } + ctx.beginPath(); + ctx.moveTo(xFrom, yFrom); + ctx.lineTo(xTo, yTo); + ctx.stroke(); + + // draw arrow at the end of the line + length = 10 + 5 * this.width; // TODO: make customizable? + ctx.arrow(xTo, yTo, angle, length); + ctx.fill(); + ctx.stroke(); + + // draw label + if (this.label) { + var point = this._pointOnLine(0.5); + this._label(ctx, this.label, point.x, point.y); + } + } + else { + // draw circle + var node = this.from; + var x, y, arrow; + var radius = this.length / 4; + if (!node.width) { + node.resize(ctx); + } + if (node.width > node.height) { + x = node.x + node.width / 2; + y = node.y - radius; + arrow = { + x: x, + y: node.y, + angle: 0.9 * Math.PI + }; } else { - // draw circle - var node = this.from; - var x, y, arrow; - var radius = this.length / 4; - if (!node.width) { - node.resize(ctx); - } - if (node.width > node.height) { - x = node.x + node.width / 2; - y = node.y - radius; - arrow = { - x: x, - y: node.y, - angle: 0.9 * Math.PI - }; - } - else { - x = node.x + radius; - y = node.y - node.height / 2; - arrow = { - x: node.x, - y: y, - angle: 0.6 * Math.PI - }; - } - ctx.beginPath(); - // TODO: do not draw a circle, but an arc - // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center - ctx.arc(x, y, radius, 0, 2 * Math.PI, false); - ctx.stroke(); - - // draw all arrows - length = 10 + 5 * this.width; // TODO: make customizable? - ctx.arrow(arrow.x, arrow.y, arrow.angle, length); - ctx.fill(); - ctx.stroke(); - - // draw label - if (this.label) { - point = this._pointOnCircle(x, y, radius, 0.5); - this._label(ctx, this.label, point.x, point.y); - } + x = node.x + radius; + y = node.y - node.height / 2; + arrow = { + x: node.x, + y: y, + angle: 0.6 * Math.PI + }; } + ctx.beginPath(); + // TODO: do not draw a circle, but an arc + // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center + ctx.arc(x, y, radius, 0, 2 * Math.PI, false); + ctx.stroke(); + + // draw all arrows + length = 10 + 5 * this.width; // TODO: make customizable? + ctx.arrow(arrow.x, arrow.y, arrow.angle, length); + ctx.fill(); + ctx.stroke(); + + // draw label + if (this.label) { + point = this._pointOnCircle(x, y, radius, 0.5); + this._label(ctx, this.label, point.x, point.y); + } + } }; @@ -575,28 +575,28 @@ Edge.prototype._drawArrow = function(ctx) { * @private */ Edge._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point - var px = x2-x1, - py = y2-y1, - something = px*px + py*py, - u = ((x3 - x1) * px + (y3 - y1) * py) / something; - - if (u > 1) { - u = 1; - } - else if (u < 0) { - u = 0; - } - - var x = x1 + u * px, - y = y1 + u * py, - dx = x - x3, - dy = y - y3; - - //# Note: If the actual distance does not matter, - //# if you only want to compare what this function - //# returns to other results of this function, you - //# can just return the squared distance instead - //# (i.e. remove the sqrt) to gain a little performance - - return Math.sqrt(dx*dx + dy*dy); + var px = x2-x1, + py = y2-y1, + something = px*px + py*py, + u = ((x3 - x1) * px + (y3 - y1) * py) / something; + + if (u > 1) { + u = 1; + } + else if (u < 0) { + u = 0; + } + + var x = x1 + u * px, + y = y1 + u * py, + dx = x - x3, + dy = y - y3; + + //# Note: If the actual distance does not matter, + //# if you only want to compare what this function + //# returns to other results of this function, you + //# can just return the squared distance instead + //# (i.e. remove the sqrt) to gain a little performance + + return Math.sqrt(dx*dx + dy*dy); }; diff --git a/src/graph/Graph.js b/src/graph/Graph.js index 7e26fc5c..8b783cb3 100644 --- a/src/graph/Graph.js +++ b/src/graph/Graph.js @@ -1,7 +1,7 @@ /** * @constructor Graph * Create a graph visualization, displaying nodes and edges. - * + * * @param {Element} container The DOM element in which the Graph will * be created. Normally a div element. * @param {Object} data An object containing parameters @@ -10,125 +10,125 @@ * @param {Object} options Options */ function Graph (container, data, options) { - // create variables and set default values - this.containerElement = container; - this.width = '100%'; - this.height = '100%'; - this.refreshRate = 50; // milliseconds - this.stabilize = true; // stabilize before displaying the graph - this.selectable = true; - - // set constant values - this.constants = { - nodes: { - radiusMin: 5, - radiusMax: 20, - radius: 5, - distance: 100, // px - shape: 'ellipse', - image: undefined, - widthMin: 16, // px - widthMax: 64, // px - fontColor: 'black', - fontSize: 14, // px - //fontFace: verdana, - fontFace: 'arial', - color: { - border: '#2B7CE9', - background: '#97C2FC', - highlight: { - border: '#2B7CE9', - background: '#D2E5FF' - } - }, - borderColor: '#2B7CE9', - backgroundColor: '#97C2FC', - highlightColor: '#D2E5FF', - group: undefined - }, - edges: { - widthMin: 1, - widthMax: 15, - width: 1, - style: 'line', - color: '#343434', - fontColor: '#343434', - fontSize: 14, // px - fontFace: 'arial', - //distance: 100, //px - length: 100, // px - dash: { - length: 10, - gap: 5, - altLength: undefined - } - }, - minForce: 0.05, - minVelocity: 0.02, // px/s - maxIterations: 1000 // maximum number of iteration to stabilize - }; - - var graph = this; - this.nodes = {}; // object with Node objects - this.edges = {}; // object with Edge objects - // TODO: create a counter to keep track on the number of nodes having values - // TODO: create a counter to keep track on the number of nodes currently moving - // TODO: create a counter to keep track on the number of edges having values - - this.nodesData = null; // A DataSet or DataView - this.edgesData = null; // A DataSet or DataView - - // create event listeners used to subscribe on the DataSets of the nodes and edges - var me = this; - this.nodesListeners = { - 'add': function (event, params) { - me._addNodes(params.items); - me.start(); - }, - 'update': function (event, params) { - me._updateNodes(params.items); - me.start(); - }, - 'remove': function (event, params) { - me._removeNodes(params.items); - me.start(); + // create variables and set default values + this.containerElement = container; + this.width = '100%'; + this.height = '100%'; + this.refreshRate = 50; // milliseconds + this.stabilize = true; // stabilize before displaying the graph + this.selectable = true; + + // set constant values + this.constants = { + nodes: { + radiusMin: 5, + radiusMax: 20, + radius: 5, + distance: 100, // px + shape: 'ellipse', + image: undefined, + widthMin: 16, // px + widthMax: 64, // px + fontColor: 'black', + fontSize: 14, // px + //fontFace: verdana, + fontFace: 'arial', + color: { + border: '#2B7CE9', + background: '#97C2FC', + highlight: { + border: '#2B7CE9', + background: '#D2E5FF' } - }; - this.edgesListeners = { - 'add': function (event, params) { - me._addEdges(params.items); - me.start(); - }, - 'update': function (event, params) { - me._updateEdges(params.items); - me.start(); - }, - 'remove': function (event, params) { - me._removeEdges(params.items); - me.start(); - } - }; - - this.groups = new Groups(); // object with groups - this.images = new Images(); // object with images - this.images.setOnloadCallback(function () { - graph._redraw(); - }); - - // properties of the data - this.moving = false; // True if any of the nodes have an undefined position - - this.selection = []; - this.timer = undefined; - - // create a frame and canvas - this._create(); - - // apply options - this.setOptions(options); - - // draw data - this.setData(data); + }, + borderColor: '#2B7CE9', + backgroundColor: '#97C2FC', + highlightColor: '#D2E5FF', + group: undefined + }, + edges: { + widthMin: 1, + widthMax: 15, + width: 1, + style: 'line', + color: '#343434', + fontColor: '#343434', + fontSize: 14, // px + fontFace: 'arial', + //distance: 100, //px + length: 100, // px + dash: { + length: 10, + gap: 5, + altLength: undefined + } + }, + minForce: 0.05, + minVelocity: 0.02, // px/s + maxIterations: 1000 // maximum number of iteration to stabilize + }; + + var graph = this; + this.nodes = {}; // object with Node objects + this.edges = {}; // object with Edge objects + // TODO: create a counter to keep track on the number of nodes having values + // TODO: create a counter to keep track on the number of nodes currently moving + // TODO: create a counter to keep track on the number of edges having values + + this.nodesData = null; // A DataSet or DataView + this.edgesData = null; // A DataSet or DataView + + // create event listeners used to subscribe on the DataSets of the nodes and edges + var me = this; + this.nodesListeners = { + 'add': function (event, params) { + me._addNodes(params.items); + me.start(); + }, + 'update': function (event, params) { + me._updateNodes(params.items); + me.start(); + }, + 'remove': function (event, params) { + me._removeNodes(params.items); + me.start(); + } + }; + this.edgesListeners = { + 'add': function (event, params) { + me._addEdges(params.items); + me.start(); + }, + 'update': function (event, params) { + me._updateEdges(params.items); + me.start(); + }, + 'remove': function (event, params) { + me._removeEdges(params.items); + me.start(); + } + }; + + this.groups = new Groups(); // object with groups + this.images = new Images(); // object with images + this.images.setOnloadCallback(function () { + graph._redraw(); + }); + + // properties of the data + this.moving = false; // True if any of the nodes have an undefined position + + this.selection = []; + this.timer = undefined; + + // create a frame and canvas + this._create(); + + // apply options + this.setOptions(options); + + // draw data + this.setData(data); } /** @@ -141,33 +141,33 @@ function Graph (container, data, options) { * {Options} [options] Object with options */ Graph.prototype.setData = function(data) { - if (data && data.dot && (data.nodes || data.edges)) { - throw new SyntaxError('Data must contain either parameter "dot" or ' + - ' parameter pair "nodes" and "edges", but not both.'); - } - - // set options - this.setOptions(data && data.options); - - // set all data - if (data && data.dot) { - // parse DOT file - if(data && data.dot) { - var dotData = vis.util.DOTToGraph(data.dot); - this.setData(dotData); - return; - } - } - else { - this._setNodes(data && data.nodes); - this._setEdges(data && data.edges); - } - - // find a stable position or start animating to a stable position - if (this.stabilize) { - this._doStabilize(); - } - this.start(); + if (data && data.dot && (data.nodes || data.edges)) { + throw new SyntaxError('Data must contain either parameter "dot" or ' + + ' parameter pair "nodes" and "edges", but not both.'); + } + + // set options + this.setOptions(data && data.options); + + // set all data + if (data && data.dot) { + // parse DOT file + if(data && data.dot) { + var dotData = vis.util.DOTToGraph(data.dot); + this.setData(dotData); + return; + } + } + else { + this._setNodes(data && data.nodes); + this._setEdges(data && data.edges); + } + + // find a stable position or start animating to a stable position + if (this.stabilize) { + this._doStabilize(); + } + this.start(); }; /** @@ -175,77 +175,77 @@ Graph.prototype.setData = function(data) { * @param {Object} options */ Graph.prototype.setOptions = function (options) { - if (options) { - // retrieve parameter values - if (options.width != undefined) {this.width = options.width;} - if (options.height != undefined) {this.height = options.height;} - if (options.stabilize != undefined) {this.stabilize = options.stabilize;} - if (options.selectable != undefined) {this.selectable = options.selectable;} - - // TODO: work out these options and document them - if (options.edges) { - for (var prop in options.edges) { - if (options.edges.hasOwnProperty(prop)) { - this.constants.edges[prop] = options.edges[prop]; - } - } - - if (options.edges.length != undefined && - options.nodes && options.nodes.distance == undefined) { - this.constants.edges.length = options.edges.length; - this.constants.nodes.distance = options.edges.length * 1.25; - } - - if (!options.edges.fontColor) { - this.constants.edges.fontColor = options.edges.color; - } - - // Added to support dashed lines - // David Jordan - // 2012-08-08 - if (options.edges.dash) { - if (options.edges.dash.length != undefined) { - this.constants.edges.dash.length = options.edges.dash.length; - } - if (options.edges.dash.gap != undefined) { - this.constants.edges.dash.gap = options.edges.dash.gap; - } - if (options.edges.dash.altLength != undefined) { - this.constants.edges.dash.altLength = options.edges.dash.altLength; - } - } + if (options) { + // retrieve parameter values + if (options.width != undefined) {this.width = options.width;} + if (options.height != undefined) {this.height = options.height;} + if (options.stabilize != undefined) {this.stabilize = options.stabilize;} + if (options.selectable != undefined) {this.selectable = options.selectable;} + + // TODO: work out these options and document them + if (options.edges) { + for (var prop in options.edges) { + if (options.edges.hasOwnProperty(prop)) { + this.constants.edges[prop] = options.edges[prop]; + } + } + + if (options.edges.length != undefined && + options.nodes && options.nodes.distance == undefined) { + this.constants.edges.length = options.edges.length; + this.constants.nodes.distance = options.edges.length * 1.25; + } + + if (!options.edges.fontColor) { + this.constants.edges.fontColor = options.edges.color; + } + + // Added to support dashed lines + // David Jordan + // 2012-08-08 + if (options.edges.dash) { + if (options.edges.dash.length != undefined) { + this.constants.edges.dash.length = options.edges.dash.length; + } + if (options.edges.dash.gap != undefined) { + this.constants.edges.dash.gap = options.edges.dash.gap; } + if (options.edges.dash.altLength != undefined) { + this.constants.edges.dash.altLength = options.edges.dash.altLength; + } + } + } - if (options.nodes) { - for (prop in options.nodes) { - if (options.nodes.hasOwnProperty(prop)) { - this.constants.nodes[prop] = options.nodes[prop]; - } - } + if (options.nodes) { + for (prop in options.nodes) { + if (options.nodes.hasOwnProperty(prop)) { + this.constants.nodes[prop] = options.nodes[prop]; + } + } - if (options.nodes.color) { - this.constants.nodes.color = Node.parseColor(options.nodes.color); - } + if (options.nodes.color) { + this.constants.nodes.color = Node.parseColor(options.nodes.color); + } - /* - if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin; - if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax; - */ - } + /* + if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin; + if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax; + */ + } - if (options.groups) { - for (var groupname in options.groups) { - if (options.groups.hasOwnProperty(groupname)) { - var group = options.groups[groupname]; - this.groups.add(groupname, group); - } - } + if (options.groups) { + for (var groupname in options.groups) { + if (options.groups.hasOwnProperty(groupname)) { + var group = options.groups[groupname]; + this.groups.add(groupname, group); } + } } + } - this.setSize(this.width, this.height); - this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2); - this._setScale(1); + this.setSize(this.width, this.height); + this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2); + this._setScale(1); }; /** @@ -255,7 +255,7 @@ Graph.prototype.setOptions = function (options) { * @private */ Graph.prototype._trigger = function (event, params) { - events.trigger(this, event, params); + events.trigger(this, event, params); }; @@ -267,48 +267,48 @@ Graph.prototype._trigger = function (event, params) { * @private */ Graph.prototype._create = function () { - // remove all elements from the container element. - while (this.containerElement.hasChildNodes()) { - this.containerElement.removeChild(this.containerElement.firstChild); - } - - this.frame = document.createElement('div'); - this.frame.className = 'graph-frame'; - this.frame.style.position = 'relative'; - this.frame.style.overflow = 'hidden'; - - // create the graph canvas (HTML canvas element) - this.frame.canvas = document.createElement( 'canvas' ); - this.frame.canvas.style.position = 'relative'; - this.frame.appendChild(this.frame.canvas); - if (!this.frame.canvas.getContext) { - var noCanvas = document.createElement( 'DIV' ); - noCanvas.style.color = 'red'; - noCanvas.style.fontWeight = 'bold' ; - noCanvas.style.padding = '10px'; - noCanvas.innerHTML = 'Error: your browser does not support HTML canvas'; - this.frame.canvas.appendChild(noCanvas); - } - - var me = this; - this.drag = {}; - this.pinch = {}; - this.hammer = Hammer(this.frame.canvas, { - prevent_default: true - }); - this.hammer.on('tap', me._onTap.bind(me) ); - this.hammer.on('hold', me._onHold.bind(me) ); - this.hammer.on('pinch', me._onPinch.bind(me) ); - this.hammer.on('touch', me._onTouch.bind(me) ); - this.hammer.on('dragstart', me._onDragStart.bind(me) ); - this.hammer.on('drag', me._onDrag.bind(me) ); - this.hammer.on('dragend', me._onDragEnd.bind(me) ); - this.hammer.on('mousewheel',me._onMouseWheel.bind(me) ); - this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF - this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) ); - - // add the frame to the container element - this.containerElement.appendChild(this.frame); + // remove all elements from the container element. + while (this.containerElement.hasChildNodes()) { + this.containerElement.removeChild(this.containerElement.firstChild); + } + + this.frame = document.createElement('div'); + this.frame.className = 'graph-frame'; + this.frame.style.position = 'relative'; + this.frame.style.overflow = 'hidden'; + + // create the graph canvas (HTML canvas element) + this.frame.canvas = document.createElement( 'canvas' ); + this.frame.canvas.style.position = 'relative'; + this.frame.appendChild(this.frame.canvas); + if (!this.frame.canvas.getContext) { + var noCanvas = document.createElement( 'DIV' ); + noCanvas.style.color = 'red'; + noCanvas.style.fontWeight = 'bold' ; + noCanvas.style.padding = '10px'; + noCanvas.innerHTML = 'Error: your browser does not support HTML canvas'; + this.frame.canvas.appendChild(noCanvas); + } + + var me = this; + this.drag = {}; + this.pinch = {}; + this.hammer = Hammer(this.frame.canvas, { + prevent_default: true + }); + this.hammer.on('tap', me._onTap.bind(me) ); + this.hammer.on('hold', me._onHold.bind(me) ); + this.hammer.on('pinch', me._onPinch.bind(me) ); + this.hammer.on('touch', me._onTouch.bind(me) ); + this.hammer.on('dragstart', me._onDragStart.bind(me) ); + this.hammer.on('drag', me._onDrag.bind(me) ); + this.hammer.on('dragend', me._onDragEnd.bind(me) ); + this.hammer.on('mousewheel',me._onMouseWheel.bind(me) ); + this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF + this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) ); + + // add the frame to the container element + this.containerElement.appendChild(this.frame); }; /** @@ -318,21 +318,21 @@ Graph.prototype._create = function () { * @private */ Graph.prototype._getNodeAt = function (pointer) { - var x = this._canvasToX(pointer.x); - var y = this._canvasToY(pointer.y); - - var obj = { - left: x, - top: y, - right: x, - bottom: y - }; - - // if there are overlapping nodes, select the last one, this is the - // one which is drawn on top of the others - var overlappingNodes = this._getNodesOverlappingWith(obj); - return (overlappingNodes.length > 0) ? - overlappingNodes[overlappingNodes.length - 1] : null; + var x = this._canvasToX(pointer.x); + var y = this._canvasToY(pointer.y); + + var obj = { + left: x, + top: y, + right: x, + bottom: y + }; + + // if there are overlapping nodes, select the last one, this is the + // one which is drawn on top of the others + var overlappingNodes = this._getNodesOverlappingWith(obj); + return (overlappingNodes.length > 0) ? + overlappingNodes[overlappingNodes.length - 1] : null; }; /** @@ -342,10 +342,10 @@ Graph.prototype._getNodeAt = function (pointer) { * @private */ Graph.prototype._getPointer = function (touch) { - return { - x: touch.pageX - vis.util.getAbsoluteLeft(this.frame.canvas), - y: touch.pageY - vis.util.getAbsoluteTop(this.frame.canvas) - }; + return { + x: touch.pageX - vis.util.getAbsoluteLeft(this.frame.canvas), + y: touch.pageY - vis.util.getAbsoluteTop(this.frame.canvas) + }; }; /** @@ -354,9 +354,9 @@ Graph.prototype._getPointer = function (touch) { * @private */ Graph.prototype._onTouch = function (event) { - this.drag.pointer = this._getPointer(event.gesture.touches[0]); - this.drag.pinched = false; - this.pinch.scale = this._getScale(); + this.drag.pointer = this._getPointer(event.gesture.touches[0]); + this.drag.pinched = false; + this.pinch.scale = this._getScale(); }; /** @@ -364,44 +364,44 @@ Graph.prototype._onTouch = function (event) { * @private */ Graph.prototype._onDragStart = function () { - var drag = this.drag; + var drag = this.drag; - drag.selection = []; - drag.translation = this._getTranslation(); - drag.nodeId = this._getNodeAt(drag.pointer); - // note: drag.pointer is set in _onTouch to get the initial touch location + drag.selection = []; + drag.translation = this._getTranslation(); + drag.nodeId = this._getNodeAt(drag.pointer); + // note: drag.pointer is set in _onTouch to get the initial touch location - var node = this.nodes[drag.nodeId]; - if (node) { - // select the clicked node if not yet selected - if (!node.isSelected()) { - this._selectNodes([drag.nodeId]); - } + var node = this.nodes[drag.nodeId]; + if (node) { + // select the clicked node if not yet selected + if (!node.isSelected()) { + this._selectNodes([drag.nodeId]); + } - // create an array with the selected nodes and their original location and status - var me = this; - this.selection.forEach(function (id) { - var node = me.nodes[id]; - if (node) { - var s = { - id: id, - node: node, - - // store original x, y, xFixed and yFixed, make the node temporarily Fixed - x: node.x, - y: node.y, - xFixed: node.xFixed, - yFixed: node.yFixed - }; - - node.xFixed = true; - node.yFixed = true; - - drag.selection.push(s); - } - }); + // create an array with the selected nodes and their original location and status + var me = this; + this.selection.forEach(function (id) { + var node = me.nodes[id]; + if (node) { + var s = { + id: id, + node: node, + + // store original x, y, xFixed and yFixed, make the node temporarily Fixed + x: node.x, + y: node.y, + xFixed: node.xFixed, + yFixed: node.yFixed + }; - } + node.xFixed = true; + node.yFixed = true; + + drag.selection.push(s); + } + }); + + } }; /** @@ -409,51 +409,51 @@ Graph.prototype._onDragStart = function () { * @private */ Graph.prototype._onDrag = function (event) { - if (this.drag.pinched) { - return; - } - - var pointer = this._getPointer(event.gesture.touches[0]); - - var me = this, - drag = this.drag, - selection = drag.selection; - if (selection && selection.length) { - // calculate delta's and new location - var deltaX = pointer.x - drag.pointer.x, - deltaY = pointer.y - drag.pointer.y; - - // update position of all selected nodes - selection.forEach(function (s) { - var node = s.node; - - if (!s.xFixed) { - node.x = me._canvasToX(me._xToCanvas(s.x) + deltaX); - } - - if (!s.yFixed) { - node.y = me._canvasToY(me._yToCanvas(s.y) + deltaY); - } - }); + if (this.drag.pinched) { + return; + } + + var pointer = this._getPointer(event.gesture.touches[0]); + + var me = this, + drag = this.drag, + selection = drag.selection; + if (selection && selection.length) { + // calculate delta's and new location + var deltaX = pointer.x - drag.pointer.x, + deltaY = pointer.y - drag.pointer.y; + + // update position of all selected nodes + selection.forEach(function (s) { + var node = s.node; + + if (!s.xFixed) { + node.x = me._canvasToX(me._xToCanvas(s.x) + deltaX); + } + + if (!s.yFixed) { + node.y = me._canvasToY(me._yToCanvas(s.y) + deltaY); + } + }); - // start animation if not yet running - if (!this.moving) { - this.moving = true; - this.start(); - } + // start animation if not yet running + if (!this.moving) { + this.moving = true; + this.start(); } - else { - // move the graph - var diffX = pointer.x - this.drag.pointer.x; - var diffY = pointer.y - this.drag.pointer.y; + } + else { + // move the graph + var diffX = pointer.x - this.drag.pointer.x; + var diffY = pointer.y - this.drag.pointer.y; - this._setTranslation( - this.drag.translation.x + diffX, - this.drag.translation.y + diffY); - this._redraw(); + this._setTranslation( + this.drag.translation.x + diffX, + this.drag.translation.y + diffY); + this._redraw(); - this.moved = true; - } + this.moved = true; + } }; /** @@ -461,14 +461,14 @@ Graph.prototype._onDrag = function (event) { * @private */ Graph.prototype._onDragEnd = function () { - var selection = this.drag.selection; - if (selection) { - selection.forEach(function (s) { - // restore original xFixed and yFixed - s.node.xFixed = s.xFixed; - s.node.yFixed = s.yFixed; - }); - } + var selection = this.drag.selection; + if (selection) { + selection.forEach(function (s) { + // restore original xFixed and yFixed + s.node.xFixed = s.xFixed; + s.node.yFixed = s.yFixed; + }); + } }; /** @@ -476,23 +476,23 @@ Graph.prototype._onDragEnd = function () { * @private */ Graph.prototype._onTap = function (event) { - var pointer = this._getPointer(event.gesture.touches[0]); + var pointer = this._getPointer(event.gesture.touches[0]); - var nodeId = this._getNodeAt(pointer); - var node = this.nodes[nodeId]; - if (node) { - // select this node - this._selectNodes([nodeId]); + var nodeId = this._getNodeAt(pointer); + var node = this.nodes[nodeId]; + if (node) { + // select this node + this._selectNodes([nodeId]); - if (!this.moving) { - this._redraw(); - } - } - else { - // remove selection - this._unselectNodes(); - this._redraw(); + if (!this.moving) { + this._redraw(); } + } + else { + // remove selection + this._unselectNodes(); + this._redraw(); + } }; /** @@ -500,26 +500,26 @@ Graph.prototype._onTap = function (event) { * @private */ Graph.prototype._onHold = function (event) { - var pointer = this._getPointer(event.gesture.touches[0]); - var nodeId = this._getNodeAt(pointer); - var node = this.nodes[nodeId]; - if (node) { - if (!node.isSelected()) { - // select this node, keep previous selection - var append = true; - this._selectNodes([nodeId], append); - } - else { - this._unselectNodes([nodeId]); - } - - if (!this.moving) { - this._redraw(); - } + var pointer = this._getPointer(event.gesture.touches[0]); + var nodeId = this._getNodeAt(pointer); + var node = this.nodes[nodeId]; + if (node) { + if (!node.isSelected()) { + // select this node, keep previous selection + var append = true; + this._selectNodes([nodeId], append); } else { - // Do nothing + this._unselectNodes([nodeId]); } + + if (!this.moving) { + this._redraw(); + } + } + else { + // Do nothing + } }; /** @@ -528,16 +528,16 @@ Graph.prototype._onHold = function (event) { * @private */ Graph.prototype._onPinch = function (event) { - var pointer = this._getPointer(event.gesture.center); + var pointer = this._getPointer(event.gesture.center); - this.drag.pinched = true; - if (!('scale' in this.pinch)) { - this.pinch.scale = 1; - } + this.drag.pinched = true; + if (!('scale' in this.pinch)) { + this.pinch.scale = 1; + } - // TODO: enable moving while pinching? - var scale = this.pinch.scale * event.gesture.scale; - this._zoom(scale, pointer) + // TODO: enable moving while pinching? + var scale = this.pinch.scale * event.gesture.scale; + this._zoom(scale, pointer) }; /** @@ -548,24 +548,24 @@ Graph.prototype._onPinch = function (event) { * @private */ Graph.prototype._zoom = function(scale, pointer) { - var scaleOld = this._getScale(); - if (scale < 0.01) { - scale = 0.01; - } - if (scale > 10) { - scale = 10; - } - - var translation = this._getTranslation(); - var scaleFrac = scale / scaleOld; - var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac; - var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac; - - this._setScale(scale); - this._setTranslation(tx, ty); - this._redraw(); - - return scale; + var scaleOld = this._getScale(); + if (scale < 0.01) { + scale = 0.01; + } + if (scale > 10) { + scale = 10; + } + + var translation = this._getTranslation(); + var scaleFrac = scale / scaleOld; + var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac; + var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac; + + this._setScale(scale); + this._setTranslation(tx, ty); + this._redraw(); + + return scale; }; /** @@ -576,45 +576,45 @@ Graph.prototype._zoom = function(scale, pointer) { * @private */ Graph.prototype._onMouseWheel = function(event) { - // retrieve delta - var 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) { - if (!('mouswheelScale' in this.pinch)) { - this.pinch.mouswheelScale = 1; - } - - // calculate the new scale - var scale = this.pinch.mouswheelScale; - var zoom = delta / 10; - if (delta < 0) { - zoom = zoom / (1 - zoom); - } - scale *= (1 + zoom); - - // calculate the pointer location - var gesture = Hammer.event.collectEventData(this, 'scroll', event); - var pointer = this._getPointer(gesture.center); + // retrieve delta + var 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) { + if (!('mouswheelScale' in this.pinch)) { + this.pinch.mouswheelScale = 1; + } + + // calculate the new scale + var scale = this.pinch.mouswheelScale; + var zoom = delta / 10; + if (delta < 0) { + zoom = zoom / (1 - zoom); + } + scale *= (1 + zoom); + + // calculate the pointer location + var gesture = Hammer.event.collectEventData(this, 'scroll', event); + var pointer = this._getPointer(gesture.center); - // apply the new scale - scale = this._zoom(scale, pointer); + // apply the new scale + scale = this._zoom(scale, pointer); - // store the new, applied scale - this.pinch.mouswheelScale = scale; - } + // store the new, applied scale + this.pinch.mouswheelScale = scale; + } - // Prevent default actions caused by mouse wheel. - event.preventDefault(); + // Prevent default actions caused by mouse wheel. + event.preventDefault(); }; @@ -624,26 +624,26 @@ Graph.prototype._onMouseWheel = function(event) { * @private */ Graph.prototype._onMouseMoveTitle = function (event) { - var gesture = Hammer.event.collectEventData(this, 'mousemove', event); - var pointer = this._getPointer(gesture.center); - - // check if the previously selected node is still selected - if (this.popupNode) { - this._checkHidePopup(pointer); - } - - // start a timeout that will check if the mouse is positioned above - // an element - var me = this; - var checkShow = function() { - me._checkShowPopup(pointer); - }; - if (this.popupTimer) { - clearInterval(this.popupTimer); // stop any running timer - } - if (!this.leftButtonDown) { - this.popupTimer = setTimeout(checkShow, 300); - } + var gesture = Hammer.event.collectEventData(this, 'mousemove', event); + var pointer = this._getPointer(gesture.center); + + // check if the previously selected node is still selected + if (this.popupNode) { + this._checkHidePopup(pointer); + } + + // start a timeout that will check if the mouse is positioned above + // an element + var me = this; + var checkShow = function() { + me._checkShowPopup(pointer); + }; + if (this.popupTimer) { + clearInterval(this.popupTimer); // stop any running timer + } + if (!this.leftButtonDown) { + this.popupTimer = setTimeout(checkShow, 300); + } }; /** @@ -655,66 +655,66 @@ Graph.prototype._onMouseMoveTitle = function (event) { * @private */ Graph.prototype._checkShowPopup = function (pointer) { - var obj = { - left: this._canvasToX(pointer.x), - top: this._canvasToY(pointer.y), - right: this._canvasToX(pointer.x), - bottom: this._canvasToY(pointer.y) - }; - - var id; - var lastPopupNode = this.popupNode; - - if (this.popupNode == undefined) { - // search the nodes for overlap, select the top one in case of multiple nodes - var nodes = this.nodes; - for (id in nodes) { - if (nodes.hasOwnProperty(id)) { - var node = nodes[id]; - if (node.getTitle() != undefined && node.isOverlappingWith(obj)) { - this.popupNode = node; - break; - } - } - } - } - - if (this.popupNode == undefined) { - // search the edges for overlap - var edges = this.edges; - for (id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; - if (edge.connected && (edge.getTitle() != undefined) && - edge.isOverlappingWith(obj)) { - this.popupNode = edge; - break; - } - } + var obj = { + left: this._canvasToX(pointer.x), + top: this._canvasToY(pointer.y), + right: this._canvasToX(pointer.x), + bottom: this._canvasToY(pointer.y) + }; + + var id; + var lastPopupNode = this.popupNode; + + if (this.popupNode == undefined) { + // search the nodes for overlap, select the top one in case of multiple nodes + var nodes = this.nodes; + for (id in nodes) { + if (nodes.hasOwnProperty(id)) { + var node = nodes[id]; + if (node.getTitle() != undefined && node.isOverlappingWith(obj)) { + this.popupNode = node; + break; } + } } + } - if (this.popupNode) { - // show popup message window - if (this.popupNode != lastPopupNode) { - var me = this; - if (!me.popup) { - me.popup = new Popup(me.frame); - } - - // adjust a small offset such that the mouse cursor is located in the - // bottom left location of the popup, and you can easily move over the - // popup area - me.popup.setPosition(pointer.x - 3, pointer.y - 3); - me.popup.setText(me.popupNode.getTitle()); - me.popup.show(); - } - } - else { - if (this.popup) { - this.popup.hide(); + if (this.popupNode == undefined) { + // search the edges for overlap + var edges = this.edges; + for (id in edges) { + if (edges.hasOwnProperty(id)) { + var edge = edges[id]; + if (edge.connected && (edge.getTitle() != undefined) && + edge.isOverlappingWith(obj)) { + this.popupNode = edge; + break; } - } + } + } + } + + if (this.popupNode) { + // show popup message window + if (this.popupNode != lastPopupNode) { + var me = this; + if (!me.popup) { + me.popup = new Popup(me.frame); + } + + // adjust a small offset such that the mouse cursor is located in the + // bottom left location of the popup, and you can easily move over the + // popup area + me.popup.setPosition(pointer.x - 3, pointer.y - 3); + me.popup.setText(me.popupNode.getTitle()); + me.popup.show(); + } + } + else { + if (this.popup) { + this.popup.hide(); + } + } }; /** @@ -724,12 +724,12 @@ Graph.prototype._checkShowPopup = function (pointer) { * @private */ Graph.prototype._checkHidePopup = function (pointer) { - if (!this.popupNode || !this._getNodeAt(pointer) ) { - this.popupNode = undefined; - if (this.popup) { - this.popup.hide(); - } + if (!this.popupNode || !this._getNodeAt(pointer) ) { + this.popupNode = undefined; + if (this.popup) { + this.popup.hide(); } + } }; /** @@ -743,43 +743,43 @@ Graph.prototype._checkHidePopup = function (pointer) { * @private */ Graph.prototype._unselectNodes = function(selection, triggerSelect) { - var changed = false; - var i, iMax, id; - - if (selection) { - // remove provided selections - for (i = 0, iMax = selection.length; i < iMax; i++) { - id = selection[i]; - this.nodes[id].unselect(); - - var j = 0; - while (j < this.selection.length) { - if (this.selection[j] == id) { - this.selection.splice(j, 1); - changed = true; - } - else { - j++; - } - } + var changed = false; + var i, iMax, id; + + if (selection) { + // remove provided selections + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; + this.nodes[id].unselect(); + + var j = 0; + while (j < this.selection.length) { + if (this.selection[j] == id) { + this.selection.splice(j, 1); + changed = true; } - } - else if (this.selection && this.selection.length) { - // remove all selections - for (i = 0, iMax = this.selection.length; i < iMax; i++) { - id = this.selection[i]; - this.nodes[id].unselect(); - changed = true; + else { + j++; } - this.selection = []; + } } - - if (changed && (triggerSelect == true || triggerSelect == undefined)) { - // fire the select event - this._trigger('select'); + } + else if (this.selection && this.selection.length) { + // remove all selections + for (i = 0, iMax = this.selection.length; i < iMax; i++) { + id = this.selection[i]; + this.nodes[id].unselect(); + changed = true; } + this.selection = []; + } - return changed; + if (changed && (triggerSelect == true || triggerSelect == undefined)) { + // fire the select event + this._trigger('select'); + } + + return changed; }; /** @@ -791,51 +791,51 @@ Graph.prototype._unselectNodes = function(selection, triggerSelect) { * @private */ Graph.prototype._selectNodes = function(selection, append) { - var changed = false; - var i, iMax; - - // TODO: the selectNodes method is a little messy, rework this - - // check if the current selection equals the desired selection - var selectionAlreadyThere = true; - if (selection.length != this.selection.length) { + var changed = false; + var i, iMax; + + // TODO: the selectNodes method is a little messy, rework this + + // check if the current selection equals the desired selection + var selectionAlreadyThere = true; + if (selection.length != this.selection.length) { + selectionAlreadyThere = false; + } + else { + for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) { + if (selection[i] != this.selection[i]) { selectionAlreadyThere = false; + break; + } } - else { - for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) { - if (selection[i] != this.selection[i]) { - selectionAlreadyThere = false; - break; - } - } - } - if (selectionAlreadyThere) { - return changed; - } - - if (append == undefined || append == false) { - // first deselect any selected node - var triggerSelect = false; - changed = this._unselectNodes(undefined, triggerSelect); - } - - for (i = 0, iMax = selection.length; i < iMax; i++) { - // add each of the new selections, but only when they are not duplicate - var id = selection[i]; - var isDuplicate = (this.selection.indexOf(id) != -1); - if (!isDuplicate) { - this.nodes[id].select(); - this.selection.push(id); - changed = true; - } - } - - if (changed) { - // fire the select event - this._trigger('select'); - } - + } + if (selectionAlreadyThere) { return changed; + } + + if (append == undefined || append == false) { + // first deselect any selected node + var triggerSelect = false; + changed = this._unselectNodes(undefined, triggerSelect); + } + + for (i = 0, iMax = selection.length; i < iMax; i++) { + // add each of the new selections, but only when they are not duplicate + var id = selection[i]; + var isDuplicate = (this.selection.indexOf(id) != -1); + if (!isDuplicate) { + this.nodes[id].select(); + this.selection.push(id); + changed = true; + } + } + + if (changed) { + // fire the select event + this._trigger('select'); + } + + return changed; }; /** @@ -845,18 +845,18 @@ Graph.prototype._selectNodes = function(selection, append) { * @private */ Graph.prototype._getNodesOverlappingWith = function (obj) { - var nodes = this.nodes, - overlappingNodes = []; + var nodes = this.nodes, + overlappingNodes = []; - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - if (nodes[id].isOverlappingWith(obj)) { - overlappingNodes.push(id); - } - } + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + if (nodes[id].isOverlappingWith(obj)) { + overlappingNodes.push(id); + } } + } - return overlappingNodes; + return overlappingNodes; }; /** @@ -865,7 +865,7 @@ Graph.prototype._getNodesOverlappingWith = function (obj) { * selected nodes. */ Graph.prototype.getSelection = function() { - return this.selection.concat([]); + return this.selection.concat([]); }; /** @@ -874,31 +874,31 @@ Graph.prototype.getSelection = function() { * selected nodes. */ Graph.prototype.setSelection = function(selection) { - var i, iMax, id; + var i, iMax, id; - if (!selection || (selection.length == undefined)) - throw 'Selection must be an array with ids'; + if (!selection || (selection.length == undefined)) + throw 'Selection must be an array with ids'; - // first unselect any selected node - for (i = 0, iMax = this.selection.length; i < iMax; i++) { - id = this.selection[i]; - this.nodes[id].unselect(); - } + // first unselect any selected node + for (i = 0, iMax = this.selection.length; i < iMax; i++) { + id = this.selection[i]; + this.nodes[id].unselect(); + } - this.selection = []; + this.selection = []; - for (i = 0, iMax = selection.length; i < iMax; i++) { - id = selection[i]; + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; - var node = this.nodes[id]; - if (!node) { - throw new RangeError('Node with id "' + id + '" not found'); - } - node.select(); - this.selection.push(id); + var node = this.nodes[id]; + if (!node) { + throw new RangeError('Node with id "' + id + '" not found'); } + node.select(); + this.selection.push(id); + } - this.redraw(); + this.redraw(); }; /** @@ -906,16 +906,16 @@ Graph.prototype.setSelection = function(selection) { * @private */ Graph.prototype._updateSelection = function () { - var i = 0; - while (i < this.selection.length) { - var id = this.selection[i]; - if (!this.nodes[id]) { - this.selection.splice(i, 1); - } - else { - i++; - } + var i = 0; + while (i < this.selection.length) { + var id = this.selection[i]; + if (!this.nodes[id]) { + this.selection.splice(i, 1); } + else { + i++; + } + } }; /** @@ -927,74 +927,74 @@ Graph.prototype._updateSelection = function () { * @private */ Graph.prototype._getConnectionCount = function(level) { - if (level == undefined) { - level = 1; - } - - // get the nodes connected to given nodes - function getConnectedNodes(nodes) { - var connectedNodes = []; - - for (var j = 0, jMax = nodes.length; j < jMax; j++) { - var node = nodes[j]; - - // find all nodes connected to this node - var edges = node.edges; - for (var i = 0, iMax = edges.length; i < iMax; i++) { - var edge = edges[i]; - var other = null; - - // check if connected - if (edge.from == node) - other = edge.to; - else if (edge.to == node) - other = edge.from; - - // check if the other node is not already in the list with nodes - var k, kMax; - if (other) { - for (k = 0, kMax = nodes.length; k < kMax; k++) { - if (nodes[k] == other) { - other = null; - break; - } - } - } - if (other) { - for (k = 0, kMax = connectedNodes.length; k < kMax; k++) { - if (connectedNodes[k] == other) { - other = null; - break; - } - } - } - - if (other) - connectedNodes.push(other); + if (level == undefined) { + level = 1; + } + + // get the nodes connected to given nodes + function getConnectedNodes(nodes) { + var connectedNodes = []; + + for (var j = 0, jMax = nodes.length; j < jMax; j++) { + var node = nodes[j]; + + // find all nodes connected to this node + var edges = node.edges; + for (var i = 0, iMax = edges.length; i < iMax; i++) { + var edge = edges[i]; + var other = null; + + // check if connected + if (edge.from == node) + other = edge.to; + else if (edge.to == node) + other = edge.from; + + // check if the other node is not already in the list with nodes + var k, kMax; + if (other) { + for (k = 0, kMax = nodes.length; k < kMax; k++) { + if (nodes[k] == other) { + other = null; + break; } + } } - - return connectedNodes; - } - - var connections = []; - var nodes = this.nodes; - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - var c = [nodes[id]]; - for (var l = 0; l < level; l++) { - c = c.concat(getConnectedNodes(c)); + if (other) { + for (k = 0, kMax = connectedNodes.length; k < kMax; k++) { + if (connectedNodes[k] == other) { + other = null; + break; } - connections.push(c); + } } + + if (other) + connectedNodes.push(other); + } } - var hubs = []; - for (var i = 0, len = connections.length; i < len; i++) { - hubs.push(connections[i].length); + return connectedNodes; + } + + var connections = []; + var nodes = this.nodes; + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + var c = [nodes[id]]; + for (var l = 0; l < level; l++) { + c = c.concat(getConnectedNodes(c)); + } + connections.push(c); } + } + + var hubs = []; + for (var i = 0, len = connections.length; i < len; i++) { + hubs.push(connections[i].length); + } - return hubs; + return hubs; }; @@ -1006,14 +1006,14 @@ Graph.prototype._getConnectionCount = function(level) { * or '30%') */ Graph.prototype.setSize = function(width, height) { - this.frame.style.width = width; - this.frame.style.height = height; + this.frame.style.width = width; + this.frame.style.height = height; - this.frame.canvas.style.width = '100%'; - this.frame.canvas.style.height = '100%'; + this.frame.canvas.style.width = '100%'; + this.frame.canvas.style.height = '100%'; - this.frame.canvas.width = this.frame.canvas.clientWidth; - this.frame.canvas.height = this.frame.canvas.clientHeight; + this.frame.canvas.width = this.frame.canvas.clientWidth; + this.frame.canvas.height = this.frame.canvas.clientHeight; }; /** @@ -1022,45 +1022,45 @@ Graph.prototype.setSize = function(width, height) { * @private */ Graph.prototype._setNodes = function(nodes) { - var oldNodesData = this.nodesData; - - if (nodes instanceof DataSet || nodes instanceof DataView) { - this.nodesData = nodes; - } - else if (nodes instanceof Array) { - this.nodesData = new DataSet(); - this.nodesData.add(nodes); - } - else if (!nodes) { - this.nodesData = new DataSet(); - } - else { - throw new TypeError('Array or DataSet expected'); - } - - if (oldNodesData) { - // unsubscribe from old dataset - util.forEach(this.nodesListeners, function (callback, event) { - oldNodesData.unsubscribe(event, callback); - }); - } + var oldNodesData = this.nodesData; + + if (nodes instanceof DataSet || nodes instanceof DataView) { + this.nodesData = nodes; + } + else if (nodes instanceof Array) { + this.nodesData = new DataSet(); + this.nodesData.add(nodes); + } + else if (!nodes) { + this.nodesData = new DataSet(); + } + else { + throw new TypeError('Array or DataSet expected'); + } + + if (oldNodesData) { + // unsubscribe from old dataset + util.forEach(this.nodesListeners, function (callback, event) { + oldNodesData.unsubscribe(event, callback); + }); + } - // remove drawn nodes - this.nodes = {}; + // remove drawn nodes + this.nodes = {}; - if (this.nodesData) { - // subscribe to new dataset - var me = this; - util.forEach(this.nodesListeners, function (callback, event) { - me.nodesData.subscribe(event, callback); - }); + if (this.nodesData) { + // subscribe to new dataset + var me = this; + util.forEach(this.nodesListeners, function (callback, event) { + me.nodesData.subscribe(event, callback); + }); - // draw all new nodes - var ids = this.nodesData.getIds(); - this._addNodes(ids); - } + // draw all new nodes + var ids = this.nodesData.getIds(); + this._addNodes(ids); + } - this._updateSelection(); + this._updateSelection(); }; /** @@ -1069,29 +1069,29 @@ Graph.prototype._setNodes = function(nodes) { * @private */ Graph.prototype._addNodes = function(ids) { - var id; - for (var i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - var data = this.nodesData.get(id); - var node = new Node(data, this.images, this.groups, this.constants); - this.nodes[id] = node; // note: this may replace an existing node - - if (!node.isFixed()) { - // TODO: position new nodes in a smarter way! - var radius = this.constants.edges.length * 2; - var count = ids.length; - var angle = 2 * Math.PI * (i / count); - node.x = radius * Math.cos(angle); - node.y = radius * Math.sin(angle); - - // note: no not use node.isMoving() here, as that gives the current - // velocity of the node, which is zero after creation of the node. - this.moving = true; - } - } - - this._reconnectEdges(); - this._updateValueRange(this.nodes); + var id; + for (var i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + var data = this.nodesData.get(id); + var node = new Node(data, this.images, this.groups, this.constants); + this.nodes[id] = node; // note: this may replace an existing node + + if (!node.isFixed()) { + // TODO: position new nodes in a smarter way! + var radius = this.constants.edges.length * 2; + var count = ids.length; + var angle = 2 * Math.PI * (i / count); + node.x = radius * Math.cos(angle); + node.y = radius * Math.sin(angle); + + // note: no not use node.isMoving() here, as that gives the current + // velocity of the node, which is zero after creation of the node. + this.moving = true; + } + } + + this._reconnectEdges(); + this._updateValueRange(this.nodes); }; /** @@ -1100,29 +1100,29 @@ Graph.prototype._addNodes = function(ids) { * @private */ Graph.prototype._updateNodes = function(ids) { - var nodes = this.nodes, - nodesData = this.nodesData; - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; - var node = nodes[id]; - var data = nodesData.get(id); - if (node) { - // update node - node.setProperties(data, this.constants); - } - else { - // create node - node = new Node(properties, this.images, this.groups, this.constants); - nodes[id] = node; + var nodes = this.nodes, + nodesData = this.nodesData; + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; + var node = nodes[id]; + var data = nodesData.get(id); + if (node) { + // update node + node.setProperties(data, this.constants); + } + else { + // create node + node = new Node(properties, this.images, this.groups, this.constants); + nodes[id] = node; - if (!node.isFixed()) { - this.moving = true; - } - } + if (!node.isFixed()) { + this.moving = true; + } } + } - this._reconnectEdges(); - this._updateValueRange(nodes); + this._reconnectEdges(); + this._updateValueRange(nodes); }; /** @@ -1131,15 +1131,15 @@ Graph.prototype._updateNodes = function(ids) { * @private */ Graph.prototype._removeNodes = function(ids) { - var nodes = this.nodes; - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; - delete nodes[id]; - } - - this._reconnectEdges(); - this._updateSelection(); - this._updateValueRange(nodes); + var nodes = this.nodes; + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; + delete nodes[id]; + } + + this._reconnectEdges(); + this._updateSelection(); + this._updateValueRange(nodes); }; /** @@ -1149,45 +1149,45 @@ Graph.prototype._removeNodes = function(ids) { * @private */ Graph.prototype._setEdges = function(edges) { - var oldEdgesData = this.edgesData; - - if (edges instanceof DataSet || edges instanceof DataView) { - this.edgesData = edges; - } - else if (edges instanceof Array) { - this.edgesData = new DataSet(); - this.edgesData.add(edges); - } - else if (!edges) { - this.edgesData = new DataSet(); - } - else { - throw new TypeError('Array or DataSet expected'); - } - - if (oldEdgesData) { - // unsubscribe from old dataset - util.forEach(this.edgesListeners, function (callback, event) { - oldEdgesData.unsubscribe(event, callback); - }); - } + var oldEdgesData = this.edgesData; + + if (edges instanceof DataSet || edges instanceof DataView) { + this.edgesData = edges; + } + else if (edges instanceof Array) { + this.edgesData = new DataSet(); + this.edgesData.add(edges); + } + else if (!edges) { + this.edgesData = new DataSet(); + } + else { + throw new TypeError('Array or DataSet expected'); + } + + if (oldEdgesData) { + // unsubscribe from old dataset + util.forEach(this.edgesListeners, function (callback, event) { + oldEdgesData.unsubscribe(event, callback); + }); + } - // remove drawn edges - this.edges = {}; + // remove drawn edges + this.edges = {}; - if (this.edgesData) { - // subscribe to new dataset - var me = this; - util.forEach(this.edgesListeners, function (callback, event) { - me.edgesData.subscribe(event, callback); - }); + if (this.edgesData) { + // subscribe to new dataset + var me = this; + util.forEach(this.edgesListeners, function (callback, event) { + me.edgesData.subscribe(event, callback); + }); - // draw all new nodes - var ids = this.edgesData.getIds(); - this._addEdges(ids); - } + // draw all new nodes + var ids = this.edgesData.getIds(); + this._addEdges(ids); + } - this._reconnectEdges(); + this._reconnectEdges(); }; /** @@ -1196,22 +1196,22 @@ Graph.prototype._setEdges = function(edges) { * @private */ Graph.prototype._addEdges = function (ids) { - var edges = this.edges, - edgesData = this.edgesData; - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; - - var oldEdge = edges[id]; - if (oldEdge) { - oldEdge.disconnect(); - } + var edges = this.edges, + edgesData = this.edgesData; + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; - var data = edgesData.get(id); - edges[id] = new Edge(data, this, this.constants); + var oldEdge = edges[id]; + if (oldEdge) { + oldEdge.disconnect(); } - this.moving = true; - this._updateValueRange(edges); + var data = edgesData.get(id); + edges[id] = new Edge(data, this, this.constants); + } + + this.moving = true; + this._updateValueRange(edges); }; /** @@ -1220,28 +1220,28 @@ Graph.prototype._addEdges = function (ids) { * @private */ Graph.prototype._updateEdges = function (ids) { - var edges = this.edges, - edgesData = this.edgesData; - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; - - var data = edgesData.get(id); - var edge = edges[id]; - if (edge) { - // update edge - edge.disconnect(); - edge.setProperties(data, this.constants); - edge.connect(); - } - else { - // create edge - edge = new Edge(data, this, this.constants); - this.edges[id] = edge; - } + var edges = this.edges, + edgesData = this.edgesData; + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; + + var data = edgesData.get(id); + var edge = edges[id]; + if (edge) { + // update edge + edge.disconnect(); + edge.setProperties(data, this.constants); + edge.connect(); + } + else { + // create edge + edge = new Edge(data, this, this.constants); + this.edges[id] = edge; } + } - this.moving = true; - this._updateValueRange(edges); + this.moving = true; + this._updateValueRange(edges); }; /** @@ -1250,18 +1250,18 @@ Graph.prototype._updateEdges = function (ids) { * @private */ Graph.prototype._removeEdges = function (ids) { - var edges = this.edges; - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; - var edge = edges[id]; - if (edge) { - edge.disconnect(); - delete edges[id]; - } - } - - this.moving = true; - this._updateValueRange(edges); + var edges = this.edges; + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; + var edge = edges[id]; + if (edge) { + edge.disconnect(); + delete edges[id]; + } + } + + this.moving = true; + this._updateValueRange(edges); }; /** @@ -1269,23 +1269,23 @@ Graph.prototype._removeEdges = function (ids) { * @private */ Graph.prototype._reconnectEdges = function() { - var id, - nodes = this.nodes, - edges = this.edges; - for (id in nodes) { - if (nodes.hasOwnProperty(id)) { - nodes[id].edges = []; - } - } - - for (id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; - edge.from = null; - edge.to = null; - edge.connect(); - } - } + var id, + nodes = this.nodes, + edges = this.edges; + for (id in nodes) { + if (nodes.hasOwnProperty(id)) { + nodes[id].edges = []; + } + } + + for (id in edges) { + if (edges.hasOwnProperty(id)) { + var edge = edges[id]; + edge.from = null; + edge.to = null; + edge.connect(); + } + } }; /** @@ -1297,29 +1297,29 @@ Graph.prototype._reconnectEdges = function() { * @private */ Graph.prototype._updateValueRange = function(obj) { - var id; - - // determine the range of the objects - var valueMin = undefined; - var valueMax = undefined; + var id; + + // determine the range of the objects + var valueMin = undefined; + var valueMax = undefined; + for (id in obj) { + if (obj.hasOwnProperty(id)) { + var value = obj[id].getValue(); + if (value !== undefined) { + valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin); + valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax); + } + } + } + + // adjust the range of all objects + if (valueMin !== undefined && valueMax !== undefined) { for (id in obj) { - if (obj.hasOwnProperty(id)) { - var value = obj[id].getValue(); - if (value !== undefined) { - valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin); - valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax); - } - } - } - - // adjust the range of all objects - if (valueMin !== undefined && valueMax !== undefined) { - for (id in obj) { - if (obj.hasOwnProperty(id)) { - obj[id].setValueRange(valueMin, valueMax); - } - } + if (obj.hasOwnProperty(id)) { + obj[id].setValueRange(valueMin, valueMax); + } } + } }; /** @@ -1327,9 +1327,9 @@ Graph.prototype._updateValueRange = function(obj) { * chart will be resized too. */ Graph.prototype.redraw = function() { - this.setSize(this.width, this.height); + this.setSize(this.width, this.height); - this._redraw(); + this._redraw(); }; /** @@ -1337,23 +1337,23 @@ Graph.prototype.redraw = function() { * @private */ Graph.prototype._redraw = function() { - var ctx = this.frame.canvas.getContext('2d'); + var ctx = this.frame.canvas.getContext('2d'); - // clear the canvas - var w = this.frame.canvas.width; - var h = this.frame.canvas.height; - ctx.clearRect(0, 0, w, h); + // clear the canvas + var w = this.frame.canvas.width; + var h = this.frame.canvas.height; + ctx.clearRect(0, 0, w, h); - // set scaling and translation - ctx.save(); - ctx.translate(this.translation.x, this.translation.y); - ctx.scale(this.scale, this.scale); + // set scaling and translation + ctx.save(); + ctx.translate(this.translation.x, this.translation.y); + ctx.scale(this.scale, this.scale); - this._drawEdges(ctx); - this._drawNodes(ctx); + this._drawEdges(ctx); + this._drawNodes(ctx); - // restore original scaling and translation - ctx.restore(); + // restore original scaling and translation + ctx.restore(); }; /** @@ -1363,19 +1363,19 @@ Graph.prototype._redraw = function() { * @private */ Graph.prototype._setTranslation = function(offsetX, offsetY) { - if (this.translation === undefined) { - this.translation = { - x: 0, - y: 0 - }; - } - - if (offsetX !== undefined) { - this.translation.x = offsetX; - } - if (offsetY !== undefined) { - this.translation.y = offsetY; - } + if (this.translation === undefined) { + this.translation = { + x: 0, + y: 0 + }; + } + + if (offsetX !== undefined) { + this.translation.x = offsetX; + } + if (offsetY !== undefined) { + this.translation.y = offsetY; + } }; /** @@ -1384,10 +1384,10 @@ Graph.prototype._setTranslation = function(offsetX, offsetY) { * @private */ Graph.prototype._getTranslation = function() { - return { - x: this.translation.x, - y: this.translation.y - }; + return { + x: this.translation.x, + y: this.translation.y + }; }; /** @@ -1396,7 +1396,7 @@ Graph.prototype._getTranslation = function() { * @private */ Graph.prototype._setScale = function(scale) { - this.scale = scale; + this.scale = scale; }; /** * Get the current scale of the graph @@ -1404,7 +1404,7 @@ Graph.prototype._setScale = function(scale) { * @private */ Graph.prototype._getScale = function() { - return this.scale; + return this.scale; }; /** @@ -1414,7 +1414,7 @@ Graph.prototype._getScale = function() { * @private */ Graph.prototype._canvasToX = function(x) { - return (x - this.translation.x) / this.scale; + return (x - this.translation.x) / this.scale; }; /** @@ -1424,7 +1424,7 @@ Graph.prototype._canvasToX = function(x) { * @private */ Graph.prototype._xToCanvas = function(x) { - return x * this.scale + this.translation.x; + return x * this.scale + this.translation.x; }; /** @@ -1434,7 +1434,7 @@ Graph.prototype._xToCanvas = function(x) { * @private */ Graph.prototype._canvasToY = function(y) { - return (y - this.translation.y) / this.scale; + return (y - this.translation.y) / this.scale; }; /** @@ -1444,7 +1444,7 @@ Graph.prototype._canvasToY = function(y) { * @private */ Graph.prototype._yToCanvas = function(y) { - return y * this.scale + this.translation.y ; + return y * this.scale + this.translation.y ; }; /** @@ -1454,24 +1454,24 @@ Graph.prototype._yToCanvas = function(y) { * @private */ Graph.prototype._drawNodes = function(ctx) { - // first draw the unselected nodes - var nodes = this.nodes; - var selected = []; - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - if (nodes[id].isSelected()) { - selected.push(id); - } - else { - nodes[id].draw(ctx); - } - } - } - - // draw the selected nodes on top - for (var s = 0, sMax = selected.length; s < sMax; s++) { - nodes[selected[s]].draw(ctx); - } + // first draw the unselected nodes + var nodes = this.nodes; + var selected = []; + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + if (nodes[id].isSelected()) { + selected.push(id); + } + else { + nodes[id].draw(ctx); + } + } + } + + // draw the selected nodes on top + for (var s = 0, sMax = selected.length; s < sMax; s++) { + nodes[selected[s]].draw(ctx); + } }; /** @@ -1481,15 +1481,15 @@ Graph.prototype._drawNodes = function(ctx) { * @private */ Graph.prototype._drawEdges = function(ctx) { - var edges = this.edges; - for (var id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; - if (edge.connected) { - edges[id].draw(ctx); - } - } - } + var edges = this.edges; + for (var id in edges) { + if (edges.hasOwnProperty(id)) { + var edge = edges[id]; + if (edge.connected) { + edges[id].draw(ctx); + } + } + } }; /** @@ -1497,22 +1497,22 @@ Graph.prototype._drawEdges = function(ctx) { * @private */ Graph.prototype._doStabilize = function() { - var start = new Date(); - - // find stable position - var count = 0; - var vmin = this.constants.minVelocity; - var stable = false; - while (!stable && count < this.constants.maxIterations) { - this._calculateForces(); - this._discreteStepNodes(); - stable = !this._isMoving(vmin); - count++; - } - - var end = new Date(); - - // console.log('Stabilized in ' + (end-start) + ' ms, ' + count + ' iterations' ); // TODO: cleanup + var start = new Date(); + + // find stable position + var count = 0; + var vmin = this.constants.minVelocity; + var stable = false; + while (!stable && count < this.constants.maxIterations) { + this._calculateForces(); + this._discreteStepNodes(); + stable = !this._isMoving(vmin); + count++; + } + + var end = new Date(); + + // console.log('Stabilized in ' + (end-start) + ' ms, ' + count + ' iterations' ); // TODO: cleanup }; /** @@ -1521,149 +1521,149 @@ Graph.prototype._doStabilize = function() { * @private */ Graph.prototype._calculateForces = function() { - // create a local edge to the nodes and edges, that is faster - var id, dx, dy, angle, distance, fx, fy, - repulsingForce, springForce, length, edgeLength, - nodes = this.nodes, - edges = this.edges; - - // gravity, add a small constant force to pull the nodes towards the center of - // the graph - // Also, the forces are reset to zero in this loop by using _setForce instead - // of _addForce - var gravity = 0.01, - gx = this.frame.canvas.clientWidth / 2, - gy = this.frame.canvas.clientHeight / 2; - for (id in nodes) { - if (nodes.hasOwnProperty(id)) { - var node = nodes[id]; - dx = gx - node.x; - dy = gy - node.y; - angle = Math.atan2(dy, dx); - fx = Math.cos(angle) * gravity; - fy = Math.sin(angle) * gravity; - - node._setForce(fx, fy); + // create a local edge to the nodes and edges, that is faster + var id, dx, dy, angle, distance, fx, fy, + repulsingForce, springForce, length, edgeLength, + nodes = this.nodes, + edges = this.edges; + + // gravity, add a small constant force to pull the nodes towards the center of + // the graph + // Also, the forces are reset to zero in this loop by using _setForce instead + // of _addForce + var gravity = 0.01, + gx = this.frame.canvas.clientWidth / 2, + gy = this.frame.canvas.clientHeight / 2; + for (id in nodes) { + if (nodes.hasOwnProperty(id)) { + var node = nodes[id]; + dx = gx - node.x; + dy = gy - node.y; + angle = Math.atan2(dy, dx); + fx = Math.cos(angle) * gravity; + fy = Math.sin(angle) * gravity; + + node._setForce(fx, fy); + } + } + + // repulsing forces between nodes + var minimumDistance = this.constants.nodes.distance, + steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance + + for (var id1 in nodes) { + if (nodes.hasOwnProperty(id1)) { + var node1 = nodes[id1]; + for (var id2 in nodes) { + if (nodes.hasOwnProperty(id2)) { + var node2 = nodes[id2]; + // calculate normally distributed force + dx = node2.x - node1.x; + dy = node2.y - node1.y; + distance = Math.sqrt(dx * dx + dy * dy); + angle = Math.atan2(dy, dx); + + // TODO: correct factor for repulsing force + //repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + //repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force + fx = Math.cos(angle) * repulsingForce; + fy = Math.sin(angle) * repulsingForce; + + node1._addForce(-fx, -fy); + node2._addForce(fx, fy); } - } - - // repulsing forces between nodes - var minimumDistance = this.constants.nodes.distance, - steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance - - for (var id1 in nodes) { - if (nodes.hasOwnProperty(id1)) { - var node1 = nodes[id1]; - for (var id2 in nodes) { - if (nodes.hasOwnProperty(id2)) { - var node2 = nodes[id2]; - // calculate normally distributed force - dx = node2.x - node1.x; - dy = node2.y - node1.y; - distance = Math.sqrt(dx * dx + dy * dy); - angle = Math.atan2(dy, dx); - - // TODO: correct factor for repulsing force - //repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force - //repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force - repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force - fx = Math.cos(angle) * repulsingForce; - fy = Math.sin(angle) * repulsingForce; - - node1._addForce(-fx, -fy); - node2._addForce(fx, fy); - } - } - } - } - - /* TODO: re-implement repulsion of edges - for (var n = 0; n < nodes.length; n++) { - for (var l = 0; l < edges.length; l++) { - var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2, - ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2, - - // calculate normally distributed force - dx = nodes[n].x - lx, - dy = nodes[n].y - ly, - distance = Math.sqrt(dx * dx + dy * dy), - angle = Math.atan2(dy, dx), - - - // TODO: correct factor for repulsing force - //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force - //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force - repulsingforce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)), // TODO: customize the repulsing force - fx = Math.cos(angle) * repulsingforce, - fy = Math.sin(angle) * repulsingforce; - nodes[n]._addForce(fx, fy); - edges[l].from._addForce(-fx/2,-fy/2); - edges[l].to._addForce(-fx/2,-fy/2); - } - } - */ - - // forces caused by the edges, modelled as springs - for (id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; - if (edge.connected) { - dx = (edge.to.x - edge.from.x); - dy = (edge.to.y - edge.from.y); - //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin - //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin - //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2; - edgeLength = edge.length; - length = Math.sqrt(dx * dx + dy * dy); - angle = Math.atan2(dy, dx); - - springForce = edge.stiffness * (edgeLength - length); - - fx = Math.cos(angle) * springForce; - fy = Math.sin(angle) * springForce; - - edge.from._addForce(-fx, -fy); - edge.to._addForce(fx, fy); - } - } - } - - /* TODO: re-implement repulsion of edges - // repulsing forces between edges - var minimumDistance = this.constants.edges.distance, - steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance - for (var l = 0; l < edges.length; l++) { - //Keep distance from other edge centers - for (var l2 = l + 1; l2 < this.edges.length; l2++) { - //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin - //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin - //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0), - var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2, - ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2, - l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2, - l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2, - - // calculate normally distributed force - dx = l2x - lx, - dy = l2y - ly, - distance = Math.sqrt(dx * dx + dy * dy), - angle = Math.atan2(dy, dx), - - - // TODO: correct factor for repulsing force - //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force - //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force - repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force - fx = Math.cos(angle) * repulsingforce, - fy = Math.sin(angle) * repulsingforce; - - edges[l].from._addForce(-fx, -fy); - edges[l].to._addForce(-fx, -fy); - edges[l2].from._addForce(fx, fy); - edges[l2].to._addForce(fx, fy); - } - } - */ + } + } + } + + /* TODO: re-implement repulsion of edges + for (var n = 0; n < nodes.length; n++) { + for (var l = 0; l < edges.length; l++) { + var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2, + ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2, + + // calculate normally distributed force + dx = nodes[n].x - lx, + dy = nodes[n].y - ly, + distance = Math.sqrt(dx * dx + dy * dy), + angle = Math.atan2(dy, dx), + + + // TODO: correct factor for repulsing force + //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force + repulsingforce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)), // TODO: customize the repulsing force + fx = Math.cos(angle) * repulsingforce, + fy = Math.sin(angle) * repulsingforce; + nodes[n]._addForce(fx, fy); + edges[l].from._addForce(-fx/2,-fy/2); + edges[l].to._addForce(-fx/2,-fy/2); + } + } + */ + + // forces caused by the edges, modelled as springs + for (id in edges) { + if (edges.hasOwnProperty(id)) { + var edge = edges[id]; + if (edge.connected) { + dx = (edge.to.x - edge.from.x); + dy = (edge.to.y - edge.from.y); + //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin + //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin + //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2; + edgeLength = edge.length; + length = Math.sqrt(dx * dx + dy * dy); + angle = Math.atan2(dy, dx); + + springForce = edge.stiffness * (edgeLength - length); + + fx = Math.cos(angle) * springForce; + fy = Math.sin(angle) * springForce; + + edge.from._addForce(-fx, -fy); + edge.to._addForce(fx, fy); + } + } + } + + /* TODO: re-implement repulsion of edges + // repulsing forces between edges + var minimumDistance = this.constants.edges.distance, + steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance + for (var l = 0; l < edges.length; l++) { + //Keep distance from other edge centers + for (var l2 = l + 1; l2 < this.edges.length; l2++) { + //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin + //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin + //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0), + var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2, + ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2, + l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2, + l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2, + + // calculate normally distributed force + dx = l2x - lx, + dy = l2y - ly, + distance = Math.sqrt(dx * dx + dy * dy), + angle = Math.atan2(dy, dx), + + + // TODO: correct factor for repulsing force + //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force + repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force + fx = Math.cos(angle) * repulsingforce, + fy = Math.sin(angle) * repulsingforce; + + edges[l].from._addForce(-fx, -fy); + edges[l].to._addForce(-fx, -fy); + edges[l2].from._addForce(fx, fy); + edges[l2].to._addForce(fx, fy); + } + } + */ }; @@ -1674,14 +1674,14 @@ Graph.prototype._calculateForces = function() { * @private */ Graph.prototype._isMoving = function(vmin) { - // TODO: ismoving does not work well: should check the kinetic energy, not its velocity - var nodes = this.nodes; - for (var id in nodes) { - if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) { - return true; - } - } - return false; + // TODO: ismoving does not work well: should check the kinetic energy, not its velocity + var nodes = this.nodes; + for (var id in nodes) { + if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) { + return true; + } + } + return false; }; @@ -1690,49 +1690,49 @@ Graph.prototype._isMoving = function(vmin) { * @private */ Graph.prototype._discreteStepNodes = function() { - var interval = this.refreshRate / 1000.0; // in seconds - var nodes = this.nodes; - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - nodes[id].discreteStep(interval); - } + var interval = this.refreshRate / 1000.0; // in seconds + var nodes = this.nodes; + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + nodes[id].discreteStep(interval); } + } }; /** * Start animating nodes and edges */ Graph.prototype.start = function() { - if (this.moving) { - this._calculateForces(); - this._discreteStepNodes(); - - var vmin = this.constants.minVelocity; - this.moving = this._isMoving(vmin); - } + if (this.moving) { + this._calculateForces(); + this._discreteStepNodes(); - if (this.moving) { - // start animation. only start timer if it is not already running - if (!this.timer) { - var graph = this; - this.timer = window.setTimeout(function () { - graph.timer = undefined; - graph.start(); - graph._redraw(); - }, this.refreshRate); - } - } - else { - this._redraw(); + var vmin = this.constants.minVelocity; + this.moving = this._isMoving(vmin); + } + + if (this.moving) { + // start animation. only start timer if it is not already running + if (!this.timer) { + var graph = this; + this.timer = window.setTimeout(function () { + graph.timer = undefined; + graph.start(); + graph._redraw(); + }, this.refreshRate); } + } + else { + this._redraw(); + } }; /** * Stop animating nodes and edges. */ Graph.prototype.stop = function () { - if (this.timer) { - window.clearInterval(this.timer); - this.timer = undefined; - } + if (this.timer) { + window.clearInterval(this.timer); + this.timer = undefined; + } }; diff --git a/src/graph/Groups.js b/src/graph/Groups.js index a33f5e47..4d92ba7d 100644 --- a/src/graph/Groups.js +++ b/src/graph/Groups.js @@ -3,8 +3,8 @@ * This class can store groups and properties specific for groups. */ Groups = function () { - this.clear(); - this.defaultIndex = 0; + this.clear(); + this.defaultIndex = 0; }; @@ -12,16 +12,16 @@ Groups = function () { * default constants for group colors */ Groups.DEFAULT = [ - {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue - {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}}, // yellow - {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}}, // red - {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}}, // green - {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}}, // magenta - {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}}, // purple - {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}}, // orange - {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue - {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}}, // pink - {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}} // mint + {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue + {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}}, // yellow + {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}}, // red + {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}}, // green + {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}}, // magenta + {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}}, // purple + {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}}, // orange + {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue + {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}}, // pink + {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}} // mint ]; @@ -29,17 +29,17 @@ Groups.DEFAULT = [ * Clear all groups */ Groups.prototype.clear = function () { - this.groups = {}; - this.groups.length = function() - { - var i = 0; - for ( var p in this ) { - if (this.hasOwnProperty(p)) { - i++; - } - } - return i; + this.groups = {}; + this.groups.length = function() + { + var i = 0; + for ( var p in this ) { + if (this.hasOwnProperty(p)) { + i++; + } } + return i; + } }; @@ -50,18 +50,18 @@ Groups.prototype.clear = function () { * @return {Object} group The created group, containing all group properties */ Groups.prototype.get = function (groupname) { - var group = this.groups[groupname]; + var group = this.groups[groupname]; - if (group == undefined) { - // create new group - var index = this.defaultIndex % Groups.DEFAULT.length; - this.defaultIndex++; - group = {}; - group.color = Groups.DEFAULT[index]; - this.groups[groupname] = group; - } + if (group == undefined) { + // create new group + var index = this.defaultIndex % Groups.DEFAULT.length; + this.defaultIndex++; + group = {}; + group.color = Groups.DEFAULT[index]; + this.groups[groupname] = group; + } - return group; + return group; }; /** @@ -72,9 +72,9 @@ Groups.prototype.get = function (groupname) { * @return {Object} group The created group object */ Groups.prototype.add = function (groupname, style) { - this.groups[groupname] = style; - if (style.color) { - style.color = Node.parseColor(style.color); - } - return style; + this.groups[groupname] = style; + if (style.color) { + style.color = Node.parseColor(style.color); + } + return style; }; diff --git a/src/graph/Images.js b/src/graph/Images.js index 6dc20d22..48517dde 100644 --- a/src/graph/Images.js +++ b/src/graph/Images.js @@ -3,9 +3,9 @@ * This class loads images and keeps them stored. */ Images = function () { - this.images = {}; + this.images = {}; - this.callback = undefined; + this.callback = undefined; }; /** @@ -14,7 +14,7 @@ Images = function () { * @param {function} callback */ Images.prototype.setOnloadCallback = function(callback) { - this.callback = callback; + this.callback = callback; }; /** @@ -23,19 +23,19 @@ Images.prototype.setOnloadCallback = function(callback) { * @return {Image} img The image object */ Images.prototype.load = function(url) { - var img = this.images[url]; - if (img == undefined) { - // create the image - var images = this; - img = new Image(); - this.images[url] = img; - img.onload = function() { - if (images.callback) { - images.callback(this); - } - }; - img.src = url; - } + var img = this.images[url]; + if (img == undefined) { + // create the image + var images = this; + img = new Image(); + this.images[url] = img; + img.onload = function() { + if (images.callback) { + images.callback(this); + } + }; + img.src = url; + } - return img; + return img; }; diff --git a/src/graph/Node.js b/src/graph/Node.js index edb10a6c..934c1399 100644 --- a/src/graph/Node.js +++ b/src/graph/Node.js @@ -23,43 +23,43 @@ * example for the color */ function Node(properties, imagelist, grouplist, constants) { - this.selected = false; + this.selected = false; - this.edges = []; // all edges connected to this node - this.group = constants.nodes.group; + this.edges = []; // all edges connected to this node + this.group = constants.nodes.group; - this.fontSize = constants.nodes.fontSize; - this.fontFace = constants.nodes.fontFace; - this.fontColor = constants.nodes.fontColor; + this.fontSize = constants.nodes.fontSize; + this.fontFace = constants.nodes.fontFace; + this.fontColor = constants.nodes.fontColor; - this.color = constants.nodes.color; + this.color = constants.nodes.color; - // set defaults for the properties - this.id = undefined; - this.shape = constants.nodes.shape; - this.image = constants.nodes.image; - this.x = 0; - this.y = 0; - this.xFixed = false; - this.yFixed = false; - this.radius = constants.nodes.radius; - this.radiusFixed = false; - this.radiusMin = constants.nodes.radiusMin; - this.radiusMax = constants.nodes.radiusMax; + // set defaults for the properties + this.id = undefined; + this.shape = constants.nodes.shape; + this.image = constants.nodes.image; + this.x = 0; + this.y = 0; + this.xFixed = false; + this.yFixed = false; + this.radius = constants.nodes.radius; + this.radiusFixed = false; + this.radiusMin = constants.nodes.radiusMin; + this.radiusMax = constants.nodes.radiusMax; - this.imagelist = imagelist; - this.grouplist = grouplist; + this.imagelist = imagelist; + this.grouplist = grouplist; - this.setProperties(properties, constants); + this.setProperties(properties, constants); - // mass, force, velocity - this.mass = 50; // kg (mass is adjusted for the number of connected edges) - this.fx = 0.0; // external force x - this.fy = 0.0; // external force y - this.vx = 0.0; // velocity x - this.vy = 0.0; // velocity y - this.minForce = constants.minForce; - this.damping = 0.9; // damping factor + // mass, force, velocity + this.mass = 50; // kg (mass is adjusted for the number of connected edges) + this.fx = 0.0; // external force x + this.fy = 0.0; // external force y + this.vx = 0.0; // velocity x + this.vy = 0.0; // velocity y + this.minForce = constants.minForce; + this.damping = 0.9; // damping factor }; /** @@ -67,10 +67,10 @@ function Node(properties, imagelist, grouplist, constants) { * @param {Edge} edge */ Node.prototype.attachEdge = function(edge) { - if (this.edges.indexOf(edge) == -1) { - this.edges.push(edge); - } - this._updateMass(); + if (this.edges.indexOf(edge) == -1) { + this.edges.push(edge); + } + this._updateMass(); }; /** @@ -78,11 +78,11 @@ Node.prototype.attachEdge = function(edge) { * @param {Edge} edge */ Node.prototype.detachEdge = function(edge) { - var index = this.edges.indexOf(edge); - if (index != -1) { - this.edges.splice(index, 1); - } - this._updateMass(); + var index = this.edges.indexOf(edge); + if (index != -1) { + this.edges.splice(index, 1); + } + this._updateMass(); }; /** @@ -91,7 +91,7 @@ Node.prototype.detachEdge = function(edge) { * @private */ Node.prototype._updateMass = function() { - this.mass = 50 + 20 * this.edges.length; // kg + this.mass = 50 + 20 * this.edges.length; // kg }; /** @@ -100,81 +100,81 @@ Node.prototype._updateMass = function() { * @param {Object} constants and object with default, global properties */ Node.prototype.setProperties = function(properties, constants) { - if (!properties) { - return; - } - - // basic properties - if (properties.id != undefined) {this.id = properties.id;} - if (properties.label != undefined) {this.label = properties.label;} - if (properties.title != undefined) {this.title = properties.title;} - if (properties.group != undefined) {this.group = properties.group;} - if (properties.x != undefined) {this.x = properties.x;} - if (properties.y != undefined) {this.y = properties.y;} - if (properties.value != undefined) {this.value = properties.value;} - - if (this.id === undefined) { - throw "Node must have an id"; - } - - // copy group properties - if (this.group) { - var groupObj = this.grouplist.get(this.group); - for (var prop in groupObj) { - if (groupObj.hasOwnProperty(prop)) { - this[prop] = groupObj[prop]; - } - } + if (!properties) { + return; + } + + // basic properties + if (properties.id != undefined) {this.id = properties.id;} + if (properties.label != undefined) {this.label = properties.label;} + if (properties.title != undefined) {this.title = properties.title;} + if (properties.group != undefined) {this.group = properties.group;} + if (properties.x != undefined) {this.x = properties.x;} + if (properties.y != undefined) {this.y = properties.y;} + if (properties.value != undefined) {this.value = properties.value;} + + if (this.id === undefined) { + throw "Node must have an id"; + } + + // copy group properties + if (this.group) { + var groupObj = this.grouplist.get(this.group); + for (var prop in groupObj) { + if (groupObj.hasOwnProperty(prop)) { + this[prop] = groupObj[prop]; + } } + } - // individual shape properties - if (properties.shape != undefined) {this.shape = properties.shape;} - if (properties.image != undefined) {this.image = properties.image;} - if (properties.radius != undefined) {this.radius = properties.radius;} - if (properties.color != undefined) {this.color = Node.parseColor(properties.color);} - - if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;} - if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;} - if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;} - + // individual shape properties + if (properties.shape != undefined) {this.shape = properties.shape;} + if (properties.image != undefined) {this.image = properties.image;} + if (properties.radius != undefined) {this.radius = properties.radius;} + if (properties.color != undefined) {this.color = Node.parseColor(properties.color);} - if (this.image != undefined) { - if (this.imagelist) { - this.imageObj = this.imagelist.load(this.image); - } - else { - throw "No imagelist provided"; - } - } + if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;} + if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;} + if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;} - this.xFixed = this.xFixed || (properties.x != undefined); - this.yFixed = this.yFixed || (properties.y != undefined); - this.radiusFixed = this.radiusFixed || (properties.radius != undefined); - if (this.shape == 'image') { - this.radiusMin = constants.nodes.widthMin; - this.radiusMax = constants.nodes.widthMax; + if (this.image != undefined) { + if (this.imagelist) { + this.imageObj = this.imagelist.load(this.image); } - - // choose draw method depending on the shape - switch (this.shape) { - case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break; - case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break; - case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break; - case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break; - // TODO: add diamond shape - case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break; - case 'text': this.draw = this._drawText; this.resize = this._resizeText; break; - case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break; - case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break; - case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break; - case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break; - case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break; - default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break; + else { + throw "No imagelist provided"; } - - // reset the size of the node, this can be changed - this._reset(); + } + + this.xFixed = this.xFixed || (properties.x != undefined); + this.yFixed = this.yFixed || (properties.y != undefined); + this.radiusFixed = this.radiusFixed || (properties.radius != undefined); + + if (this.shape == 'image') { + this.radiusMin = constants.nodes.widthMin; + this.radiusMax = constants.nodes.widthMax; + } + + // choose draw method depending on the shape + switch (this.shape) { + case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break; + case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break; + case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break; + case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break; + // TODO: add diamond shape + case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break; + case 'text': this.draw = this._drawText; this.resize = this._resizeText; break; + case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break; + case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break; + case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break; + case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break; + case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break; + default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break; + } + + // reset the size of the node, this can be changed + this._reset(); }; /** @@ -184,51 +184,51 @@ Node.prototype.setProperties = function(properties, constants) { * @return {Object} colorObject */ Node.parseColor = function(color) { - var c; - if (util.isString(color)) { - c = { - border: color, - background: color, - highlight: { - border: color, - background: color - } - }; - // TODO: automatically generate a nice highlight color + var c; + if (util.isString(color)) { + c = { + border: color, + background: color, + highlight: { + border: color, + background: color + } + }; + // TODO: automatically generate a nice highlight color + } + else { + c = {}; + c.background = color.background || 'white'; + c.border = color.border || c.background; + if (util.isString(color.highlight)) { + c.highlight = { + border: color.highlight, + background: color.highlight + } } else { - c = {}; - c.background = color.background || 'white'; - c.border = color.border || c.background; - if (util.isString(color.highlight)) { - c.highlight = { - border: color.highlight, - background: color.highlight - } - } - else { - c.highlight = {}; - c.highlight.background = color.highlight && color.highlight.background || c.background; - c.highlight.border = color.highlight && color.highlight.border || c.border; - } + c.highlight = {}; + c.highlight.background = color.highlight && color.highlight.background || c.background; + c.highlight.border = color.highlight && color.highlight.border || c.border; } - return c; + } + return c; }; /** * select this node */ Node.prototype.select = function() { - this.selected = true; - this._reset(); + this.selected = true; + this._reset(); }; /** * unselect this node */ Node.prototype.unselect = function() { - this.selected = false; - this._reset(); + this.selected = false; + this._reset(); }; /** @@ -236,8 +236,8 @@ Node.prototype.unselect = function() { * @private */ Node.prototype._reset = function() { - this.width = undefined; - this.height = undefined; + this.width = undefined; + this.height = undefined; }; /** @@ -246,7 +246,7 @@ Node.prototype._reset = function() { * has been set. */ Node.prototype.getTitle = function() { - return this.title; + return this.title; }; /** @@ -256,46 +256,46 @@ Node.prototype.getTitle = function() { * @returns {number} distance Distance to the border in pixels */ Node.prototype.distanceToBorder = function (ctx, angle) { - var borderWidth = 1; - - if (!this.width) { - this.resize(ctx); - } - - //noinspection FallthroughInSwitchStatementJS - switch (this.shape) { - case 'circle': - case 'dot': - return this.radius + borderWidth; - - case 'ellipse': - 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); - - // TODO: implement distanceToBorder for database - // TODO: implement distanceToBorder for triangle - // TODO: implement distanceToBorder for triangleDown - - case 'box': - case 'image': - case 'text': - default: - if (this.width) { - return Math.min( - Math.abs(this.width / 2 / Math.cos(angle)), - Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth; - // TODO: reckon with border radius too in case of box - } - else { - return 0; - } + var borderWidth = 1; + + if (!this.width) { + this.resize(ctx); + } + + //noinspection FallthroughInSwitchStatementJS + switch (this.shape) { + case 'circle': + case 'dot': + return this.radius + borderWidth; + + case 'ellipse': + 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); + + // TODO: implement distanceToBorder for database + // TODO: implement distanceToBorder for triangle + // TODO: implement distanceToBorder for triangleDown + + case 'box': + case 'image': + case 'text': + default: + if (this.width) { + return Math.min( + Math.abs(this.width / 2 / Math.cos(angle)), + Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth; + // TODO: reckon with border radius too in case of box + } + else { + return 0; + } - } + } - // TODO: implement calculation of distance to border for all shapes + // TODO: implement calculation of distance to border for all shapes }; /** @@ -304,8 +304,8 @@ Node.prototype.distanceToBorder = function (ctx, angle) { * @param {number} fy Force in vertical direction */ Node.prototype._setForce = function(fx, fy) { - this.fx = fx; - this.fy = fy; + this.fx = fx; + this.fy = fy; }; /** @@ -315,8 +315,8 @@ Node.prototype._setForce = function(fx, fy) { * @private */ Node.prototype._addForce = function(fx, fy) { - this.fx += fx; - this.fy += fy; + this.fx += fx; + this.fy += fy; }; /** @@ -324,19 +324,19 @@ Node.prototype._addForce = function(fx, fy) { * @param {number} interval Time interval in seconds */ Node.prototype.discreteStep = function(interval) { - if (!this.xFixed) { - var dx = -this.damping * this.vx; // damping force - var ax = (this.fx + dx) / this.mass; // acceleration - this.vx += ax / interval; // velocity - this.x += this.vx / interval; // position - } + if (!this.xFixed) { + var dx = -this.damping * this.vx; // damping force + var ax = (this.fx + dx) / this.mass; // acceleration + this.vx += ax / interval; // velocity + this.x += this.vx / interval; // position + } - if (!this.yFixed) { - var dy = -this.damping * this.vy; // damping force - var ay = (this.fy + dy) / this.mass; // acceleration - this.vy += ay / interval; // velocity - this.y += this.vy / interval; // position - } + if (!this.yFixed) { + var dy = -this.damping * this.vy; // damping force + var ay = (this.fy + dy) / this.mass; // acceleration + this.vy += ay / interval; // velocity + this.y += this.vy / interval; // position + } }; @@ -345,7 +345,7 @@ Node.prototype.discreteStep = function(interval) { * @return {boolean} true if fixed, false if not */ Node.prototype.isFixed = function() { - return (this.xFixed && this.yFixed); + return (this.xFixed && this.yFixed); }; /** @@ -355,9 +355,9 @@ Node.prototype.isFixed = function() { */ // TODO: replace this method with calculating the kinetic energy Node.prototype.isMoving = function(vmin) { - return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin || - (!this.xFixed && Math.abs(this.fx) > this.minForce) || - (!this.yFixed && Math.abs(this.fy) > this.minForce)); + return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin || + (!this.xFixed && Math.abs(this.fx) > this.minForce) || + (!this.yFixed && Math.abs(this.fy) > this.minForce)); }; /** @@ -365,7 +365,7 @@ Node.prototype.isMoving = function(vmin) { * @return {boolean} selected True if node is selected, else false */ Node.prototype.isSelected = function() { - return this.selected; + return this.selected; }; /** @@ -373,7 +373,7 @@ Node.prototype.isSelected = function() { * @return {Number} value */ Node.prototype.getValue = function() { - return this.value; + return this.value; }; /** @@ -383,9 +383,9 @@ Node.prototype.getValue = function() { * @return {Number} value */ Node.prototype.getDistance = function(x, y) { - var dx = this.x - x, - dy = this.y - y; - return Math.sqrt(dx * dx + dy * dy); + var dx = this.x - x, + dy = this.y - y; + return Math.sqrt(dx * dx + dy * dy); }; @@ -396,15 +396,15 @@ Node.prototype.getDistance = function(x, y) { * @param {Number} max */ Node.prototype.setValueRange = function(min, max) { - if (!this.radiusFixed && this.value !== undefined) { - if (max == min) { - this.radius = (this.radiusMin + this.radiusMax) / 2; - } - else { - var scale = (this.radiusMax - this.radiusMin) / (max - min); - this.radius = (this.value - min) * scale + this.radiusMin; - } + if (!this.radiusFixed && this.value !== undefined) { + if (max == min) { + this.radius = (this.radiusMin + this.radiusMax) / 2; + } + else { + var scale = (this.radiusMax - this.radiusMin) / (max - min); + this.radius = (this.value - min) * scale + this.radiusMin; } + } }; /** @@ -413,7 +413,7 @@ Node.prototype.setValueRange = function(min, max) { * @param {CanvasRenderingContext2D} ctx */ Node.prototype.draw = function(ctx) { - throw "Draw method not initialized for node"; + throw "Draw method not initialized for node"; }; /** @@ -422,7 +422,7 @@ Node.prototype.draw = function(ctx) { * @param {CanvasRenderingContext2D} ctx */ Node.prototype.resize = function(ctx) { - throw "Resize method not initialized for node"; + throw "Resize method not initialized for node"; }; /** @@ -431,256 +431,256 @@ Node.prototype.resize = function(ctx) { * @return {boolean} True if location is located on node */ Node.prototype.isOverlappingWith = function(obj) { - return (this.left < obj.right && - this.left + this.width > obj.left && - this.top < obj.bottom && - this.top + this.height > obj.top); + return (this.left < obj.right && + this.left + this.width > obj.left && + this.top < obj.bottom && + this.top + this.height > obj.top); }; Node.prototype._resizeImage = function (ctx) { - // TODO: pre calculate the image size - if (!this.width) { // undefined or 0 - var width, height; - if (this.value) { - var scale = this.imageObj.height / this.imageObj.width; - width = this.radius || this.imageObj.width; - height = this.radius * scale || this.imageObj.height; - } - else { - width = this.imageObj.width; - height = this.imageObj.height; - } - this.width = width; - this.height = height; + // TODO: pre calculate the image size + if (!this.width) { // undefined or 0 + var width, height; + if (this.value) { + var scale = this.imageObj.height / this.imageObj.width; + width = this.radius || this.imageObj.width; + height = this.radius * scale || this.imageObj.height; } + else { + width = this.imageObj.width; + height = this.imageObj.height; + } + this.width = width; + this.height = height; + } }; Node.prototype._drawImage = function (ctx) { - this._resizeImage(ctx); + this._resizeImage(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; - var yLabel; - if (this.imageObj) { - ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height); - yLabel = this.y + this.height / 2; - } - else { - // image still loading... just draw the label for now - yLabel = this.y; - } + var yLabel; + if (this.imageObj) { + ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height); + yLabel = this.y + this.height / 2; + } + else { + // image still loading... just draw the label for now + yLabel = this.y; + } - this._label(ctx, this.label, this.x, yLabel, undefined, "top"); + this._label(ctx, this.label, this.x, yLabel, undefined, "top"); }; Node.prototype._resizeBox = function (ctx) { - if (!this.width) { - var margin = 5; - var textSize = this.getTextSize(ctx); - this.width = textSize.width + 2 * margin; - this.height = textSize.height + 2 * margin; - } + if (!this.width) { + var margin = 5; + var textSize = this.getTextSize(ctx); + this.width = textSize.width + 2 * margin; + this.height = textSize.height + 2 * margin; + } }; Node.prototype._drawBox = function (ctx) { - this._resizeBox(ctx); + this._resizeBox(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; - ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; - ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; - ctx.lineWidth = this.selected ? 2.0 : 1.0; - ctx.roundRect(this.left, this.top, this.width, this.height, this.radius); - ctx.fill(); - ctx.stroke(); + ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; + ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; + ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.roundRect(this.left, this.top, this.width, this.height, this.radius); + ctx.fill(); + ctx.stroke(); - this._label(ctx, this.label, this.x, this.y); + this._label(ctx, this.label, this.x, this.y); }; Node.prototype._resizeDatabase = function (ctx) { - if (!this.width) { - var margin = 5; - var textSize = this.getTextSize(ctx); - var size = textSize.width + 2 * margin; - this.width = size; - this.height = size; - } + if (!this.width) { + var margin = 5; + var textSize = this.getTextSize(ctx); + var size = textSize.width + 2 * margin; + this.width = size; + this.height = size; + } }; Node.prototype._drawDatabase = function (ctx) { - this._resizeDatabase(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; + this._resizeDatabase(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; - ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; - ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; - ctx.lineWidth = this.selected ? 2.0 : 1.0; - ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height); - ctx.fill(); - ctx.stroke(); + ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; + ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; + ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height); + ctx.fill(); + ctx.stroke(); - this._label(ctx, this.label, this.x, this.y); + this._label(ctx, this.label, this.x, this.y); }; Node.prototype._resizeCircle = function (ctx) { - if (!this.width) { - var margin = 5; - var textSize = this.getTextSize(ctx); - var diameter = Math.max(textSize.width, textSize.height) + 2 * margin; - this.radius = diameter / 2; - - this.width = diameter; - this.height = diameter; - } + if (!this.width) { + var margin = 5; + var textSize = this.getTextSize(ctx); + var diameter = Math.max(textSize.width, textSize.height) + 2 * margin; + this.radius = diameter / 2; + + this.width = diameter; + this.height = diameter; + } }; Node.prototype._drawCircle = function (ctx) { - this._resizeCircle(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; + this._resizeCircle(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; - ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; - ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; - ctx.lineWidth = this.selected ? 2.0 : 1.0; - ctx.circle(this.x, this.y, this.radius); - ctx.fill(); - ctx.stroke(); + ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; + ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; + ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.circle(this.x, this.y, this.radius); + ctx.fill(); + ctx.stroke(); - this._label(ctx, this.label, this.x, this.y); + this._label(ctx, this.label, this.x, this.y); }; Node.prototype._resizeEllipse = function (ctx) { - if (!this.width) { - var textSize = this.getTextSize(ctx); - - this.width = textSize.width * 1.5; - this.height = textSize.height * 2; - if (this.width < this.height) { - this.width = this.height; - } + if (!this.width) { + var textSize = this.getTextSize(ctx); + + this.width = textSize.width * 1.5; + this.height = textSize.height * 2; + if (this.width < this.height) { + this.width = this.height; } + } }; Node.prototype._drawEllipse = function (ctx) { - this._resizeEllipse(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; + this._resizeEllipse(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; - ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; - ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; - ctx.lineWidth = this.selected ? 2.0 : 1.0; - ctx.ellipse(this.left, this.top, this.width, this.height); - ctx.fill(); - ctx.stroke(); + ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; + ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; + ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.ellipse(this.left, this.top, this.width, this.height); + ctx.fill(); + ctx.stroke(); - this._label(ctx, this.label, this.x, this.y); + this._label(ctx, this.label, this.x, this.y); }; Node.prototype._drawDot = function (ctx) { - this._drawShape(ctx, 'circle'); + this._drawShape(ctx, 'circle'); }; Node.prototype._drawTriangle = function (ctx) { - this._drawShape(ctx, 'triangle'); + this._drawShape(ctx, 'triangle'); }; Node.prototype._drawTriangleDown = function (ctx) { - this._drawShape(ctx, 'triangleDown'); + this._drawShape(ctx, 'triangleDown'); }; Node.prototype._drawSquare = function (ctx) { - this._drawShape(ctx, 'square'); + this._drawShape(ctx, 'square'); }; Node.prototype._drawStar = function (ctx) { - this._drawShape(ctx, 'star'); + this._drawShape(ctx, 'star'); }; Node.prototype._resizeShape = function (ctx) { - if (!this.width) { - var size = 2 * this.radius; - this.width = size; - this.height = size; - } + if (!this.width) { + var size = 2 * this.radius; + this.width = size; + this.height = size; + } }; Node.prototype._drawShape = function (ctx, shape) { - this._resizeShape(ctx); + this._resizeShape(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; - ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; - ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; - ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; + ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; + ctx.lineWidth = this.selected ? 2.0 : 1.0; - ctx[shape](this.x, this.y, this.radius); - ctx.fill(); - ctx.stroke(); + ctx[shape](this.x, this.y, this.radius); + ctx.fill(); + ctx.stroke(); - if (this.label) { - this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top'); - } + if (this.label) { + this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top'); + } }; Node.prototype._resizeText = function (ctx) { - if (!this.width) { - var margin = 5; - var textSize = this.getTextSize(ctx); - this.width = textSize.width + 2 * margin; - this.height = textSize.height + 2 * margin; - } + if (!this.width) { + var margin = 5; + var textSize = this.getTextSize(ctx); + this.width = textSize.width + 2 * margin; + this.height = textSize.height + 2 * margin; + } }; Node.prototype._drawText = function (ctx) { - this._resizeText(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; + this._resizeText(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; - this._label(ctx, this.label, this.x, this.y); + this._label(ctx, this.label, this.x, this.y); }; Node.prototype._label = function (ctx, text, x, y, align, baseline) { - if (text) { - ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace; - ctx.fillStyle = this.fontColor || "black"; - ctx.textAlign = align || "center"; - ctx.textBaseline = baseline || "middle"; - - var lines = text.split('\n'), - lineCount = lines.length, - fontSize = (this.fontSize + 4), - yLine = y + (1 - lineCount) / 2 * fontSize; - - for (var i = 0; i < lineCount; i++) { - ctx.fillText(lines[i], x, yLine); - yLine += fontSize; - } + if (text) { + ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace; + ctx.fillStyle = this.fontColor || "black"; + ctx.textAlign = align || "center"; + ctx.textBaseline = baseline || "middle"; + + var lines = text.split('\n'), + lineCount = lines.length, + fontSize = (this.fontSize + 4), + yLine = y + (1 - lineCount) / 2 * fontSize; + + for (var i = 0; i < lineCount; i++) { + ctx.fillText(lines[i], x, yLine); + yLine += fontSize; } + } }; Node.prototype.getTextSize = function(ctx) { - if (this.label != undefined) { - ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace; - - var lines = this.label.split('\n'), - height = (this.fontSize + 4) * lines.length, - width = 0; + if (this.label != undefined) { + ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace; - for (var i = 0, iMax = lines.length; i < iMax; i++) { - width = Math.max(width, ctx.measureText(lines[i]).width); - } + var lines = this.label.split('\n'), + height = (this.fontSize + 4) * lines.length, + width = 0; - return {"width": width, "height": height}; - } - else { - return {"width": 0, "height": 0}; + for (var i = 0, iMax = lines.length; i < iMax; i++) { + width = Math.max(width, ctx.measureText(lines[i]).width); } + + return {"width": width, "height": height}; + } + else { + return {"width": 0, "height": 0}; + } }; diff --git a/src/graph/Popup.js b/src/graph/Popup.js index 2d0121a3..7dc8ffe8 100644 --- a/src/graph/Popup.js +++ b/src/graph/Popup.js @@ -6,38 +6,38 @@ * @param {String} [text] */ function Popup(container, x, y, text) { - if (container) { - this.container = container; - } - else { - this.container = document.body; - } - this.x = 0; - this.y = 0; - this.padding = 5; + if (container) { + this.container = container; + } + else { + this.container = document.body; + } + this.x = 0; + this.y = 0; + this.padding = 5; - if (x !== undefined && y !== undefined ) { - this.setPosition(x, y); - } - if (text !== undefined) { - this.setText(text); - } + if (x !== undefined && y !== undefined ) { + this.setPosition(x, y); + } + if (text !== undefined) { + this.setText(text); + } - // create the frame - this.frame = document.createElement("div"); - var style = this.frame.style; - style.position = "absolute"; - style.visibility = "hidden"; - style.border = "1px solid #666"; - style.color = "black"; - style.padding = this.padding + "px"; - style.backgroundColor = "#FFFFC6"; - style.borderRadius = "3px"; - style.MozBorderRadius = "3px"; - style.WebkitBorderRadius = "3px"; - style.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)"; - style.whiteSpace = "nowrap"; - this.container.appendChild(this.frame); + // create the frame + this.frame = document.createElement("div"); + var style = this.frame.style; + style.position = "absolute"; + style.visibility = "hidden"; + style.border = "1px solid #666"; + style.color = "black"; + style.padding = this.padding + "px"; + style.backgroundColor = "#FFFFC6"; + style.borderRadius = "3px"; + style.MozBorderRadius = "3px"; + style.WebkitBorderRadius = "3px"; + style.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)"; + style.whiteSpace = "nowrap"; + this.container.appendChild(this.frame); }; /** @@ -45,8 +45,8 @@ function Popup(container, x, y, text) { * @param {number} y Vertical position of the popup window */ Popup.prototype.setPosition = function(x, y) { - this.x = parseInt(x); - this.y = parseInt(y); + this.x = parseInt(x); + this.y = parseInt(y); }; /** @@ -54,7 +54,7 @@ Popup.prototype.setPosition = function(x, y) { * @param {string} text */ Popup.prototype.setText = function(text) { - this.frame.innerHTML = text; + this.frame.innerHTML = text; }; /** @@ -62,44 +62,44 @@ Popup.prototype.setText = function(text) { * @param {boolean} show Optional. Show or hide the window */ Popup.prototype.show = function (show) { - if (show === undefined) { - show = true; - } + if (show === undefined) { + show = true; + } - if (show) { - var height = this.frame.clientHeight; - var width = this.frame.clientWidth; - var maxHeight = this.frame.parentNode.clientHeight; - var maxWidth = this.frame.parentNode.clientWidth; + if (show) { + var height = this.frame.clientHeight; + var width = this.frame.clientWidth; + var maxHeight = this.frame.parentNode.clientHeight; + var maxWidth = this.frame.parentNode.clientWidth; - var top = (this.y - height); - if (top + height + this.padding > maxHeight) { - top = maxHeight - height - this.padding; - } - if (top < this.padding) { - top = this.padding; - } - - var left = this.x; - if (left + width + this.padding > maxWidth) { - left = maxWidth - width - this.padding; - } - if (left < this.padding) { - left = this.padding; - } + var top = (this.y - height); + if (top + height + this.padding > maxHeight) { + top = maxHeight - height - this.padding; + } + if (top < this.padding) { + top = this.padding; + } - this.frame.style.left = left + "px"; - this.frame.style.top = top + "px"; - this.frame.style.visibility = "visible"; + var left = this.x; + if (left + width + this.padding > maxWidth) { + left = maxWidth - width - this.padding; } - else { - this.hide(); + if (left < this.padding) { + left = this.padding; } + + this.frame.style.left = left + "px"; + this.frame.style.top = top + "px"; + this.frame.style.visibility = "visible"; + } + else { + this.hide(); + } }; /** * Hide the popup window */ Popup.prototype.hide = function () { - this.frame.style.visibility = "hidden"; + this.frame.style.visibility = "hidden"; }; diff --git a/src/graph/dotparser.js b/src/graph/dotparser.js index 46d73d05..71576079 100644 --- a/src/graph/dotparser.js +++ b/src/graph/dotparser.js @@ -1,829 +1,829 @@ (function(exports) { - /** - * Parse a text source containing data in DOT language into a JSON object. - * The object contains two lists: one with nodes and one with edges. - * - * DOT language reference: http://www.graphviz.org/doc/info/lang.html - * - * @param {String} data Text containing a graph in DOT-notation - * @return {Object} graph An object containing two parameters: - * {Object[]} nodes - * {Object[]} edges - */ - function parseDOT (data) { - dot = data; - return parseGraph(); - } - - // token types enumeration - var TOKENTYPE = { - NULL : 0, - DELIMITER : 1, - IDENTIFIER: 2, - UNKNOWN : 3 - }; - - // map with all delimiters - var DELIMITERS = { - '{': true, - '}': true, - '[': true, - ']': true, - ';': true, - '=': true, - ',': true, - - '->': true, - '--': true - }; - - var dot = ''; // current dot file - var index = 0; // current index in dot file - var c = ''; // current token character in expr - var token = ''; // current token - var tokenType = TOKENTYPE.NULL; // type of the token - - /** - * Get the first character from the dot file. - * The character is stored into the char c. If the end of the dot file is - * reached, the function puts an empty string in c. - */ - function first() { - index = 0; - c = dot.charAt(0); - } - - /** - * Get the next character from the dot file. - * The character is stored into the char c. If the end of the dot file is - * reached, the function puts an empty string in c. - */ - function next() { - index++; - c = dot.charAt(index); - } - - /** - * Preview the next character from the dot file. - * @return {String} cNext - */ - function nextPreview() { - return dot.charAt(index + 1); - } - - /** - * Test whether given character is alphabetic or numeric - * @param {String} c - * @return {Boolean} isAlphaNumeric - */ - var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/; - function isAlphaNumeric(c) { - return regexAlphaNumeric.test(c); - } - - /** - * Merge all properties of object b into object b - * @param {Object} a - * @param {Object} b - * @return {Object} a - */ - function merge (a, b) { - if (!a) { - a = {}; - } - - if (b) { - for (var name in b) { - if (b.hasOwnProperty(name)) { - a[name] = b[name]; - } - } - } - return a; - } - - /** - * Set a value in an object, where the provided parameter name can be a - * path with nested parameters. For example: - * - * var obj = {a: 2}; - * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}} - * - * @param {Object} obj - * @param {String} path A parameter name or dot-separated parameter path, - * like "color.highlight.border". - * @param {*} value - */ - function setValue(obj, path, value) { - var keys = path.split('.'); - var o = obj; - while (keys.length) { - var key = keys.shift(); - if (keys.length) { - // this isn't the end point - if (!o[key]) { - o[key] = {}; - } - o = o[key]; - } - else { - // this is the end point - o[key] = value; - } - } + /** + * Parse a text source containing data in DOT language into a JSON object. + * The object contains two lists: one with nodes and one with edges. + * + * DOT language reference: http://www.graphviz.org/doc/info/lang.html + * + * @param {String} data Text containing a graph in DOT-notation + * @return {Object} graph An object containing two parameters: + * {Object[]} nodes + * {Object[]} edges + */ + function parseDOT (data) { + dot = data; + return parseGraph(); + } + + // token types enumeration + var TOKENTYPE = { + NULL : 0, + DELIMITER : 1, + IDENTIFIER: 2, + UNKNOWN : 3 + }; + + // map with all delimiters + var DELIMITERS = { + '{': true, + '}': true, + '[': true, + ']': true, + ';': true, + '=': true, + ',': true, + + '->': true, + '--': true + }; + + var dot = ''; // current dot file + var index = 0; // current index in dot file + var c = ''; // current token character in expr + var token = ''; // current token + var tokenType = TOKENTYPE.NULL; // type of the token + + /** + * Get the first character from the dot file. + * The character is stored into the char c. If the end of the dot file is + * reached, the function puts an empty string in c. + */ + function first() { + index = 0; + c = dot.charAt(0); + } + + /** + * Get the next character from the dot file. + * The character is stored into the char c. If the end of the dot file is + * reached, the function puts an empty string in c. + */ + function next() { + index++; + c = dot.charAt(index); + } + + /** + * Preview the next character from the dot file. + * @return {String} cNext + */ + function nextPreview() { + return dot.charAt(index + 1); + } + + /** + * Test whether given character is alphabetic or numeric + * @param {String} c + * @return {Boolean} isAlphaNumeric + */ + var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/; + function isAlphaNumeric(c) { + return regexAlphaNumeric.test(c); + } + + /** + * Merge all properties of object b into object b + * @param {Object} a + * @param {Object} b + * @return {Object} a + */ + function merge (a, b) { + if (!a) { + a = {}; } - /** - * Add a node to a graph object. If there is already a node with - * the same id, their attributes will be merged. - * @param {Object} graph - * @param {Object} node - */ - function addNode(graph, node) { - var i, len; - var current = null; - - // find root graph (in case of subgraph) - var graphs = [graph]; // list with all graphs from current graph to root graph - var root = graph; - while (root.parent) { - graphs.push(root.parent); - root = root.parent; - } - - // find existing node (at root level) by its id - if (root.nodes) { - for (i = 0, len = root.nodes.length; i < len; i++) { - if (node.id === root.nodes[i].id) { - current = root.nodes[i]; - break; - } - } - } - - if (!current) { - // this is a new node - current = { - id: node.id - }; - if (graph.node) { - // clone default attributes - current.attr = merge(current.attr, graph.node); - } - } - - // add node to this (sub)graph and all its parent graphs - for (i = graphs.length - 1; i >= 0; i--) { - var g = graphs[i]; - - if (!g.nodes) { - g.nodes = []; - } - if (g.nodes.indexOf(current) == -1) { - g.nodes.push(current); - } - } - - // merge attributes - if (node.attr) { - current.attr = merge(current.attr, node.attr); + if (b) { + for (var name in b) { + if (b.hasOwnProperty(name)) { + a[name] = b[name]; } + } + } + return a; + } + + /** + * Set a value in an object, where the provided parameter name can be a + * path with nested parameters. For example: + * + * var obj = {a: 2}; + * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}} + * + * @param {Object} obj + * @param {String} path A parameter name or dot-separated parameter path, + * like "color.highlight.border". + * @param {*} value + */ + function setValue(obj, path, value) { + var keys = path.split('.'); + var o = obj; + while (keys.length) { + var key = keys.shift(); + if (keys.length) { + // this isn't the end point + if (!o[key]) { + o[key] = {}; + } + o = o[key]; + } + else { + // this is the end point + o[key] = value; + } + } + } + + /** + * Add a node to a graph object. If there is already a node with + * the same id, their attributes will be merged. + * @param {Object} graph + * @param {Object} node + */ + function addNode(graph, node) { + var i, len; + var current = null; + + // find root graph (in case of subgraph) + var graphs = [graph]; // list with all graphs from current graph to root graph + var root = graph; + while (root.parent) { + graphs.push(root.parent); + root = root.parent; } - /** - * Add an edge to a graph object - * @param {Object} graph - * @param {Object} edge - */ - function addEdge(graph, edge) { - if (!graph.edges) { - graph.edges = []; - } - graph.edges.push(edge); - if (graph.edge) { - var attr = merge({}, graph.edge); // clone default attributes - edge.attr = merge(attr, edge.attr); // merge attributes + // find existing node (at root level) by its id + if (root.nodes) { + for (i = 0, len = root.nodes.length; i < len; i++) { + if (node.id === root.nodes[i].id) { + current = root.nodes[i]; + break; } + } } - /** - * Create an edge to a graph object - * @param {Object} graph - * @param {String | Number | Object} from - * @param {String | Number | Object} to - * @param {String} type - * @param {Object | null} attr - * @return {Object} edge - */ - function createEdge(graph, from, to, type, attr) { - var edge = { - from: from, - to: to, - type: type - }; + if (!current) { + // this is a new node + current = { + id: node.id + }; + if (graph.node) { + // clone default attributes + current.attr = merge(current.attr, graph.node); + } + } - if (graph.edge) { - edge.attr = merge({}, graph.edge); // clone default attributes - } - edge.attr = merge(edge.attr || {}, attr); // merge attributes + // add node to this (sub)graph and all its parent graphs + for (i = graphs.length - 1; i >= 0; i--) { + var g = graphs[i]; - return edge; + if (!g.nodes) { + g.nodes = []; + } + if (g.nodes.indexOf(current) == -1) { + g.nodes.push(current); + } } - /** - * Get next token in the current dot file. - * The token and token type are available as token and tokenType - */ - function getToken() { - tokenType = TOKENTYPE.NULL; - token = ''; + // merge attributes + if (node.attr) { + current.attr = merge(current.attr, node.attr); + } + } + + /** + * Add an edge to a graph object + * @param {Object} graph + * @param {Object} edge + */ + function addEdge(graph, edge) { + if (!graph.edges) { + graph.edges = []; + } + graph.edges.push(edge); + if (graph.edge) { + var attr = merge({}, graph.edge); // clone default attributes + edge.attr = merge(attr, edge.attr); // merge attributes + } + } + + /** + * Create an edge to a graph object + * @param {Object} graph + * @param {String | Number | Object} from + * @param {String | Number | Object} to + * @param {String} type + * @param {Object | null} attr + * @return {Object} edge + */ + function createEdge(graph, from, to, type, attr) { + var edge = { + from: from, + to: to, + type: type + }; - // skip over whitespaces - while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter - next(); - } + if (graph.edge) { + edge.attr = merge({}, graph.edge); // clone default attributes + } + edge.attr = merge(edge.attr || {}, attr); // merge attributes + + return edge; + } + + /** + * Get next token in the current dot file. + * The token and token type are available as token and tokenType + */ + function getToken() { + tokenType = TOKENTYPE.NULL; + token = ''; + + // skip over whitespaces + while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter + next(); + } - do { - var isComment = false; - - // skip comment - if (c == '#') { - // find the previous non-space character - var i = index - 1; - while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') { - i--; - } - if (dot.charAt(i) == '\n' || dot.charAt(i) == '') { - // the # is at the start of a line, this is indeed a line comment - while (c != '' && c != '\n') { - next(); - } - isComment = true; - } - } - if (c == '/' && nextPreview() == '/') { - // skip line comment - while (c != '' && c != '\n') { - next(); - } - isComment = true; - } - if (c == '/' && nextPreview() == '*') { - // skip block comment - while (c != '') { - if (c == '*' && nextPreview() == '/') { - // end of block comment found. skip these last two characters - next(); - next(); - break; - } - else { - next(); - } - } - isComment = true; - } - - // skip over whitespaces - while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter - next(); - } - } - while (isComment); + do { + var isComment = false; - // check for end of dot file - if (c == '') { - // token is still empty - tokenType = TOKENTYPE.DELIMITER; - return; + // skip comment + if (c == '#') { + // find the previous non-space character + var i = index - 1; + while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') { + i--; } - - // check for delimiters consisting of 2 characters - var c2 = c + nextPreview(); - if (DELIMITERS[c2]) { - tokenType = TOKENTYPE.DELIMITER; - token = c2; + if (dot.charAt(i) == '\n' || dot.charAt(i) == '') { + // the # is at the start of a line, this is indeed a line comment + while (c != '' && c != '\n') { next(); + } + isComment = true; + } + } + if (c == '/' && nextPreview() == '/') { + // skip line comment + while (c != '' && c != '\n') { + next(); + } + isComment = true; + } + if (c == '/' && nextPreview() == '*') { + // skip block comment + while (c != '') { + if (c == '*' && nextPreview() == '/') { + // end of block comment found. skip these last two characters next(); - return; - } - - // check for delimiters consisting of 1 character - if (DELIMITERS[c]) { - tokenType = TOKENTYPE.DELIMITER; - token = c; next(); - return; + break; + } + else { + next(); + } } + isComment = true; + } - // check for an identifier (number or string) - // TODO: more precise parsing of numbers/strings (and the port separator ':') - if (isAlphaNumeric(c) || c == '-') { - token += c; - next(); + // skip over whitespaces + while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter + next(); + } + } + while (isComment); - while (isAlphaNumeric(c)) { - token += c; - next(); - } - if (token == 'false') { - token = false; // convert to boolean - } - else if (token == 'true') { - token = true; // convert to boolean - } - else if (!isNaN(Number(token))) { - token = Number(token); // convert to number - } - tokenType = TOKENTYPE.IDENTIFIER; - return; - } + // check for end of dot file + if (c == '') { + // token is still empty + tokenType = TOKENTYPE.DELIMITER; + return; + } - // check for a string enclosed by double quotes - if (c == '"') { - next(); - while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) { - token += c; - if (c == '"') { // skip the escape character - next(); - } - next(); - } - if (c != '"') { - throw newSyntaxError('End of string " expected'); - } - next(); - tokenType = TOKENTYPE.IDENTIFIER; - return; - } + // check for delimiters consisting of 2 characters + var c2 = c + nextPreview(); + if (DELIMITERS[c2]) { + tokenType = TOKENTYPE.DELIMITER; + token = c2; + next(); + next(); + return; + } - // something unknown is found, wrong characters, a syntax error - tokenType = TOKENTYPE.UNKNOWN; - while (c != '') { - token += c; - next(); - } - throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"'); + // check for delimiters consisting of 1 character + if (DELIMITERS[c]) { + tokenType = TOKENTYPE.DELIMITER; + token = c; + next(); + return; } - /** - * Parse a graph. - * @returns {Object} graph - */ - function parseGraph() { - var graph = {}; + // check for an identifier (number or string) + // TODO: more precise parsing of numbers/strings (and the port separator ':') + if (isAlphaNumeric(c) || c == '-') { + token += c; + next(); + + while (isAlphaNumeric(c)) { + token += c; + next(); + } + if (token == 'false') { + token = false; // convert to boolean + } + else if (token == 'true') { + token = true; // convert to boolean + } + else if (!isNaN(Number(token))) { + token = Number(token); // convert to number + } + tokenType = TOKENTYPE.IDENTIFIER; + return; + } - first(); - getToken(); + // check for a string enclosed by double quotes + if (c == '"') { + next(); + while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) { + token += c; + if (c == '"') { // skip the escape character + next(); + } + next(); + } + if (c != '"') { + throw newSyntaxError('End of string " expected'); + } + next(); + tokenType = TOKENTYPE.IDENTIFIER; + return; + } - // optional strict keyword - if (token == 'strict') { - graph.strict = true; - getToken(); - } + // something unknown is found, wrong characters, a syntax error + tokenType = TOKENTYPE.UNKNOWN; + while (c != '') { + token += c; + next(); + } + throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"'); + } + + /** + * Parse a graph. + * @returns {Object} graph + */ + function parseGraph() { + var graph = {}; + + first(); + getToken(); + + // optional strict keyword + if (token == 'strict') { + graph.strict = true; + getToken(); + } - // graph or digraph keyword - if (token == 'graph' || token == 'digraph') { - graph.type = token; - getToken(); - } + // graph or digraph keyword + if (token == 'graph' || token == 'digraph') { + graph.type = token; + getToken(); + } - // optional graph id - if (tokenType == TOKENTYPE.IDENTIFIER) { - graph.id = token; - getToken(); - } + // optional graph id + if (tokenType == TOKENTYPE.IDENTIFIER) { + graph.id = token; + getToken(); + } - // open angle bracket - if (token != '{') { - throw newSyntaxError('Angle bracket { expected'); - } - getToken(); + // open angle bracket + if (token != '{') { + throw newSyntaxError('Angle bracket { expected'); + } + getToken(); - // statements - parseStatements(graph); + // statements + parseStatements(graph); - // close angle bracket - if (token != '}') { - throw newSyntaxError('Angle bracket } expected'); - } - getToken(); + // close angle bracket + if (token != '}') { + throw newSyntaxError('Angle bracket } expected'); + } + getToken(); - // end of file - if (token !== '') { - throw newSyntaxError('End of file expected'); - } + // end of file + if (token !== '') { + throw newSyntaxError('End of file expected'); + } + getToken(); + + // remove temporary default properties + delete graph.node; + delete graph.edge; + delete graph.graph; + + return graph; + } + + /** + * Parse a list with statements. + * @param {Object} graph + */ + function parseStatements (graph) { + while (token !== '' && token != '}') { + parseStatement(graph); + if (token == ';') { getToken(); + } + } + } + + /** + * Parse a single statement. Can be a an attribute statement, node + * statement, a series of node statements and edge statements, or a + * parameter. + * @param {Object} graph + */ + function parseStatement(graph) { + // parse subgraph + var subgraph = parseSubgraph(graph); + if (subgraph) { + // edge statements + parseEdge(graph, subgraph); + + return; + } - // remove temporary default properties - delete graph.node; - delete graph.edge; - delete graph.graph; + // parse an attribute statement + var attr = parseAttributeStatement(graph); + if (attr) { + return; + } - return graph; + // parse node + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError('Identifier expected'); + } + var id = token; // id can be a string or a number + getToken(); + + if (token == '=') { + // id statement + getToken(); + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError('Identifier expected'); + } + graph[id] = token; + getToken(); + // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] " + } + else { + parseNodeStatement(graph, id); + } + } + + /** + * Parse a subgraph + * @param {Object} graph parent graph object + * @return {Object | null} subgraph + */ + function parseSubgraph (graph) { + var subgraph = null; + + // optional subgraph keyword + if (token == 'subgraph') { + subgraph = {}; + subgraph.type = 'subgraph'; + getToken(); + + // optional graph id + if (tokenType == TOKENTYPE.IDENTIFIER) { + subgraph.id = token; + getToken(); + } } - /** - * Parse a list with statements. - * @param {Object} graph - */ - function parseStatements (graph) { - while (token !== '' && token != '}') { - parseStatement(graph); - if (token == ';') { - getToken(); - } - } + // open angle bracket + if (token == '{') { + getToken(); + + if (!subgraph) { + subgraph = {}; + } + subgraph.parent = graph; + subgraph.node = graph.node; + subgraph.edge = graph.edge; + subgraph.graph = graph.graph; + + // statements + parseStatements(subgraph); + + // close angle bracket + if (token != '}') { + throw newSyntaxError('Angle bracket } expected'); + } + getToken(); + + // remove temporary default properties + delete subgraph.node; + delete subgraph.edge; + delete subgraph.graph; + delete subgraph.parent; + + // register at the parent graph + if (!graph.subgraphs) { + graph.subgraphs = []; + } + graph.subgraphs.push(subgraph); } - /** - * Parse a single statement. Can be a an attribute statement, node - * statement, a series of node statements and edge statements, or a - * parameter. - * @param {Object} graph - */ - function parseStatement(graph) { - // parse subgraph - var subgraph = parseSubgraph(graph); - if (subgraph) { - // edge statements - parseEdge(graph, subgraph); - - return; - } + return subgraph; + } + + /** + * parse an attribute statement like "node [shape=circle fontSize=16]". + * Available keywords are 'node', 'edge', 'graph'. + * The previous list with default attributes will be replaced + * @param {Object} graph + * @returns {String | null} keyword Returns the name of the parsed attribute + * (node, edge, graph), or null if nothing + * is parsed. + */ + function parseAttributeStatement (graph) { + // attribute statements + if (token == 'node') { + getToken(); + + // node attributes + graph.node = parseAttributeList(); + return 'node'; + } + else if (token == 'edge') { + getToken(); - // parse an attribute statement - var attr = parseAttributeStatement(graph); - if (attr) { - return; - } + // edge attributes + graph.edge = parseAttributeList(); + return 'edge'; + } + else if (token == 'graph') { + getToken(); + + // graph attributes + graph.graph = parseAttributeList(); + return 'graph'; + } - // parse node + return null; + } + + /** + * parse a node statement + * @param {Object} graph + * @param {String | Number} id + */ + function parseNodeStatement(graph, id) { + // node statement + var node = { + id: id + }; + var attr = parseAttributeList(); + if (attr) { + node.attr = attr; + } + addNode(graph, node); + + // edge statements + parseEdge(graph, id); + } + + /** + * Parse an edge or a series of edges + * @param {Object} graph + * @param {String | Number} from Id of the from node + */ + function parseEdge(graph, from) { + while (token == '->' || token == '--') { + var to; + var type = token; + getToken(); + + var subgraph = parseSubgraph(graph); + if (subgraph) { + to = subgraph; + } + else { if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError('Identifier expected'); + throw newSyntaxError('Identifier or subgraph expected'); } - var id = token; // id can be a string or a number + to = token; + addNode(graph, { + id: to + }); getToken(); + } - if (token == '=') { - // id statement - getToken(); - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError('Identifier expected'); - } - graph[id] = token; - getToken(); - // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] " - } - else { - parseNodeStatement(graph, id); - } - } + // parse edge attributes + var attr = parseAttributeList(); - /** - * Parse a subgraph - * @param {Object} graph parent graph object - * @return {Object | null} subgraph - */ - function parseSubgraph (graph) { - var subgraph = null; - - // optional subgraph keyword - if (token == 'subgraph') { - subgraph = {}; - subgraph.type = 'subgraph'; - getToken(); - - // optional graph id - if (tokenType == TOKENTYPE.IDENTIFIER) { - subgraph.id = token; - getToken(); - } - } + // create edge + var edge = createEdge(graph, from, to, type, attr); + addEdge(graph, edge); - // open angle bracket - if (token == '{') { - getToken(); - - if (!subgraph) { - subgraph = {}; - } - subgraph.parent = graph; - subgraph.node = graph.node; - subgraph.edge = graph.edge; - subgraph.graph = graph.graph; - - // statements - parseStatements(subgraph); - - // close angle bracket - if (token != '}') { - throw newSyntaxError('Angle bracket } expected'); - } - getToken(); - - // remove temporary default properties - delete subgraph.node; - delete subgraph.edge; - delete subgraph.graph; - delete subgraph.parent; - - // register at the parent graph - if (!graph.subgraphs) { - graph.subgraphs = []; - } - graph.subgraphs.push(subgraph); + from = to; + } + } + + /** + * Parse a set with attributes, + * for example [label="1.000", shape=solid] + * @return {Object | null} attr + */ + function parseAttributeList() { + var attr = null; + + while (token == '[') { + getToken(); + attr = {}; + while (token !== '' && token != ']') { + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError('Attribute name expected'); } + var name = token; - return subgraph; - } - - /** - * parse an attribute statement like "node [shape=circle fontSize=16]". - * Available keywords are 'node', 'edge', 'graph'. - * The previous list with default attributes will be replaced - * @param {Object} graph - * @returns {String | null} keyword Returns the name of the parsed attribute - * (node, edge, graph), or null if nothing - * is parsed. - */ - function parseAttributeStatement (graph) { - // attribute statements - if (token == 'node') { - getToken(); - - // node attributes - graph.node = parseAttributeList(); - return 'node'; + getToken(); + if (token != '=') { + throw newSyntaxError('Equal sign = expected'); } - else if (token == 'edge') { - getToken(); + getToken(); - // edge attributes - graph.edge = parseAttributeList(); - return 'edge'; + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError('Attribute value expected'); } - else if (token == 'graph') { - getToken(); + var value = token; + setValue(attr, name, value); // name can be a path - // graph attributes - graph.graph = parseAttributeList(); - return 'graph'; + getToken(); + if (token ==',') { + getToken(); } + } - return null; + if (token != ']') { + throw newSyntaxError('Bracket ] expected'); + } + getToken(); } - /** - * parse a node statement - * @param {Object} graph - * @param {String | Number} id - */ - function parseNodeStatement(graph, id) { - // node statement - var node = { - id: id - }; - var attr = parseAttributeList(); - if (attr) { - node.attr = attr; + return attr; + } + + /** + * Create a syntax error with extra information on current token and index. + * @param {String} message + * @returns {SyntaxError} err + */ + function newSyntaxError(message) { + return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')'); + } + + /** + * Chop off text after a maximum length + * @param {String} text + * @param {Number} maxLength + * @returns {String} + */ + function chop (text, maxLength) { + return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...'); + } + + /** + * Execute a function fn for each pair of elements in two arrays + * @param {Array | *} array1 + * @param {Array | *} array2 + * @param {function} fn + */ + function forEach2(array1, array2, fn) { + if (array1 instanceof Array) { + array1.forEach(function (elem1) { + if (array2 instanceof Array) { + array2.forEach(function (elem2) { + fn(elem1, elem2); + }); } - addNode(graph, node); - - // edge statements - parseEdge(graph, id); - } - - /** - * Parse an edge or a series of edges - * @param {Object} graph - * @param {String | Number} from Id of the from node - */ - function parseEdge(graph, from) { - while (token == '->' || token == '--') { - var to; - var type = token; - getToken(); - - var subgraph = parseSubgraph(graph); - if (subgraph) { - to = subgraph; - } - else { - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError('Identifier or subgraph expected'); - } - to = token; - addNode(graph, { - id: to - }); - getToken(); - } - - // parse edge attributes - var attr = parseAttributeList(); - - // create edge - var edge = createEdge(graph, from, to, type, attr); - addEdge(graph, edge); - - from = to; + else { + fn(elem1, array2); } + }); + } + else { + if (array2 instanceof Array) { + array2.forEach(function (elem2) { + fn(array1, elem2); + }); + } + else { + fn(array1, array2); + } } + } + + /** + * Convert a string containing a graph in DOT language into a map containing + * with nodes and edges in the format of graph. + * @param {String} data Text containing a graph in DOT-notation + * @return {Object} graphData + */ + function DOTToGraph (data) { + // parse the DOT file + var dotData = parseDOT(data); + var graphData = { + nodes: [], + edges: [], + options: {} + }; - /** - * Parse a set with attributes, - * for example [label="1.000", shape=solid] - * @return {Object | null} attr - */ - function parseAttributeList() { - var attr = null; - - while (token == '[') { - getToken(); - attr = {}; - while (token !== '' && token != ']') { - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError('Attribute name expected'); - } - var name = token; - - getToken(); - if (token != '=') { - throw newSyntaxError('Equal sign = expected'); - } - getToken(); - - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError('Attribute value expected'); - } - var value = token; - setValue(attr, name, value); // name can be a path - - getToken(); - if (token ==',') { - getToken(); - } - } - - if (token != ']') { - throw newSyntaxError('Bracket ] expected'); - } - getToken(); + // copy the nodes + if (dotData.nodes) { + dotData.nodes.forEach(function (dotNode) { + var graphNode = { + id: dotNode.id, + label: String(dotNode.label || dotNode.id) + }; + merge(graphNode, dotNode.attr); + if (graphNode.image) { + graphNode.shape = 'image'; } + graphData.nodes.push(graphNode); + }); + } - return attr; - } - - /** - * Create a syntax error with extra information on current token and index. - * @param {String} message - * @returns {SyntaxError} err - */ - function newSyntaxError(message) { - return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')'); - } - - /** - * Chop off text after a maximum length - * @param {String} text - * @param {Number} maxLength - * @returns {String} - */ - function chop (text, maxLength) { - return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...'); - } - - /** - * Execute a function fn for each pair of elements in two arrays - * @param {Array | *} array1 - * @param {Array | *} array2 - * @param {function} fn - */ - function forEach2(array1, array2, fn) { - if (array1 instanceof Array) { - array1.forEach(function (elem1) { - if (array2 instanceof Array) { - array2.forEach(function (elem2) { - fn(elem1, elem2); - }); - } - else { - fn(elem1, array2); - } - }); - } - else { - if (array2 instanceof Array) { - array2.forEach(function (elem2) { - fn(array1, elem2); - }); - } - else { - fn(array1, array2); - } - } - } - - /** - * Convert a string containing a graph in DOT language into a map containing - * with nodes and edges in the format of graph. - * @param {String} data Text containing a graph in DOT-notation - * @return {Object} graphData - */ - function DOTToGraph (data) { - // parse the DOT file - var dotData = parseDOT(data); - var graphData = { - nodes: [], - edges: [], - options: {} + // copy the edges + if (dotData.edges) { + /** + * Convert an edge in DOT format to an edge with VisGraph format + * @param {Object} dotEdge + * @returns {Object} graphEdge + */ + function convertEdge(dotEdge) { + var graphEdge = { + from: dotEdge.from, + to: dotEdge.to }; + merge(graphEdge, dotEdge.attr); + graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line'; + return graphEdge; + } - // copy the nodes - if (dotData.nodes) { - dotData.nodes.forEach(function (dotNode) { - var graphNode = { - id: dotNode.id, - label: String(dotNode.label || dotNode.id) - }; - merge(graphNode, dotNode.attr); - if (graphNode.image) { - graphNode.shape = 'image'; - } - graphData.nodes.push(graphNode); - }); + dotData.edges.forEach(function (dotEdge) { + var from, to; + if (dotEdge.from instanceof Object) { + from = dotEdge.from.nodes; + } + else { + from = { + id: dotEdge.from + } } - // copy the edges - if (dotData.edges) { - /** - * Convert an edge in DOT format to an edge with VisGraph format - * @param {Object} dotEdge - * @returns {Object} graphEdge - */ - function convertEdge(dotEdge) { - var graphEdge = { - from: dotEdge.from, - to: dotEdge.to - }; - merge(graphEdge, dotEdge.attr); - graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line'; - return graphEdge; - } - - dotData.edges.forEach(function (dotEdge) { - var from, to; - if (dotEdge.from instanceof Object) { - from = dotEdge.from.nodes; - } - else { - from = { - id: dotEdge.from - } - } - - if (dotEdge.to instanceof Object) { - to = dotEdge.to.nodes; - } - else { - to = { - id: dotEdge.to - } - } - - if (dotEdge.from instanceof Object && dotEdge.from.edges) { - dotEdge.from.edges.forEach(function (subEdge) { - var graphEdge = convertEdge(subEdge); - graphData.edges.push(graphEdge); - }); - } - - forEach2(from, to, function (from, to) { - var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr); - var graphEdge = convertEdge(subEdge); - graphData.edges.push(graphEdge); - }); - - if (dotEdge.to instanceof Object && dotEdge.to.edges) { - dotEdge.to.edges.forEach(function (subEdge) { - var graphEdge = convertEdge(subEdge); - graphData.edges.push(graphEdge); - }); - } - }); + if (dotEdge.to instanceof Object) { + to = dotEdge.to.nodes; + } + else { + to = { + id: dotEdge.to + } + } + + if (dotEdge.from instanceof Object && dotEdge.from.edges) { + dotEdge.from.edges.forEach(function (subEdge) { + var graphEdge = convertEdge(subEdge); + graphData.edges.push(graphEdge); + }); } - // copy the options - if (dotData.attr) { - graphData.options = dotData.attr; + forEach2(from, to, function (from, to) { + var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr); + var graphEdge = convertEdge(subEdge); + graphData.edges.push(graphEdge); + }); + + if (dotEdge.to instanceof Object && dotEdge.to.edges) { + dotEdge.to.edges.forEach(function (subEdge) { + var graphEdge = convertEdge(subEdge); + graphData.edges.push(graphEdge); + }); } + }); + } - return graphData; + // copy the options + if (dotData.attr) { + graphData.options = dotData.attr; } - // exports - exports.parseDOT = parseDOT; - exports.DOTToGraph = DOTToGraph; + return graphData; + } + + // exports + exports.parseDOT = parseDOT; + exports.DOTToGraph = DOTToGraph; })(typeof util !== 'undefined' ? util : exports); diff --git a/src/graph/shapes.js b/src/graph/shapes.js index 93919306..ed80372b 100644 --- a/src/graph/shapes.js +++ b/src/graph/shapes.js @@ -3,223 +3,223 @@ */ if (typeof CanvasRenderingContext2D !== 'undefined') { - /** - * Draw a circle shape - */ - CanvasRenderingContext2D.prototype.circle = function(x, y, r) { - this.beginPath(); - this.arc(x, y, r, 0, 2*Math.PI, false); - }; - - /** - * Draw a square shape - * @param {Number} x horizontal center - * @param {Number} y vertical center - * @param {Number} r size, width and height of the square - */ - CanvasRenderingContext2D.prototype.square = function(x, y, r) { - this.beginPath(); - this.rect(x - r, y - r, r * 2, r * 2); - }; - - /** - * Draw a triangle shape - * @param {Number} x horizontal center - * @param {Number} y vertical center - * @param {Number} r radius, half the length of the sides of the triangle - */ - CanvasRenderingContext2D.prototype.triangle = function(x, y, r) { - // http://en.wikipedia.org/wiki/Equilateral_triangle - this.beginPath(); - - var s = r * 2; - var s2 = s / 2; - var ir = Math.sqrt(3) / 6 * s; // radius of inner circle - var h = Math.sqrt(s * s - s2 * s2); // height - - this.moveTo(x, y - (h - ir)); - this.lineTo(x + s2, y + ir); - this.lineTo(x - s2, y + ir); - this.lineTo(x, y - (h - ir)); - this.closePath(); - }; - - /** - * Draw a triangle shape in downward orientation - * @param {Number} x horizontal center - * @param {Number} y vertical center - * @param {Number} r radius - */ - CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) { - // http://en.wikipedia.org/wiki/Equilateral_triangle - this.beginPath(); - - var s = r * 2; - var s2 = s / 2; - var ir = Math.sqrt(3) / 6 * s; // radius of inner circle - var h = Math.sqrt(s * s - s2 * s2); // height - - this.moveTo(x, y + (h - ir)); - this.lineTo(x + s2, y - ir); - this.lineTo(x - s2, y - ir); - this.lineTo(x, y + (h - ir)); - this.closePath(); - }; - - /** - * Draw a star shape, a star with 5 points - * @param {Number} x horizontal center - * @param {Number} y vertical center - * @param {Number} r radius, half the length of the sides of the triangle - */ - CanvasRenderingContext2D.prototype.star = function(x, y, r) { - // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/ - this.beginPath(); - - for (var n = 0; n < 10; n++) { - var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5; - this.lineTo( - x + radius * Math.sin(n * 2 * Math.PI / 10), - y - radius * Math.cos(n * 2 * Math.PI / 10) - ); - } - - this.closePath(); - }; - - /** - * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas - */ - CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) { - var r2d = Math.PI/180; - if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x - if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y - this.beginPath(); - this.moveTo(x+r,y); - this.lineTo(x+w-r,y); - this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false); - this.lineTo(x+w,y+h-r); - this.arc(x+w-r,y+h-r,r,0,r2d*90,false); - this.lineTo(x+r,y+h); - this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false); - this.lineTo(x,y+r); - this.arc(x+r,y+r,r,r2d*180,r2d*270,false); - }; - - /** - * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas - */ - CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) { - var kappa = .5522848, - ox = (w / 2) * kappa, // control point offset horizontal - oy = (h / 2) * kappa, // control point offset vertical - xe = x + w, // x-end - ye = y + h, // y-end - xm = x + w / 2, // x-middle - ym = y + h / 2; // y-middle - - this.beginPath(); - this.moveTo(x, ym); - this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); - this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); - this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); - this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); - }; - - - - /** - * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas - */ - CanvasRenderingContext2D.prototype.database = function(x, y, w, h) { - var f = 1/3; - var wEllipse = w; - var hEllipse = h * f; - - var kappa = .5522848, - ox = (wEllipse / 2) * kappa, // control point offset horizontal - oy = (hEllipse / 2) * kappa, // control point offset vertical - xe = x + wEllipse, // x-end - ye = y + hEllipse, // y-end - xm = x + wEllipse / 2, // x-middle - ym = y + hEllipse / 2, // y-middle - ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse - yeb = y + h; // y-end, bottom ellipse - - this.beginPath(); - this.moveTo(xe, ym); - - this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); - this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); - - this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); - this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); - - this.lineTo(xe, ymb); - - this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb); - this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb); - - this.lineTo(x, ym); - }; - - - /** - * Draw an arrow point (no line) - */ - CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) { - // tail - var xt = x - length * Math.cos(angle); - var yt = y - length * Math.sin(angle); - - // inner tail - // TODO: allow to customize different shapes - var xi = x - length * 0.9 * Math.cos(angle); - var yi = y - length * 0.9 * Math.sin(angle); - - // left - var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI); - var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI); - - // right - var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI); - var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI); - - this.beginPath(); - this.moveTo(x, y); - this.lineTo(xl, yl); - this.lineTo(xi, yi); - this.lineTo(xr, yr); - this.closePath(); - }; - - /** - * Sets up the dashedLine functionality for drawing - * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas - * @author David Jordan - * @date 2012-08-08 - */ - CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){ - if (!dashArray) dashArray=[10,5]; - if (dashLength==0) dashLength = 0.001; // Hack for Safari - var dashCount = dashArray.length; - this.moveTo(x, y); - var dx = (x2-x), dy = (y2-y); - var slope = dy/dx; - var distRemaining = Math.sqrt( dx*dx + dy*dy ); - var dashIndex=0, draw=true; - while (distRemaining>=0.1){ - var dashLength = dashArray[dashIndex++%dashCount]; - if (dashLength > distRemaining) dashLength = distRemaining; - var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) ); - if (dx<0) xStep = -xStep; - x += xStep; - y += slope*xStep; - this[draw ? 'lineTo' : 'moveTo'](x,y); - distRemaining -= dashLength; - draw = !draw; - } - }; - - // TODO: add diamond shape + /** + * Draw a circle shape + */ + CanvasRenderingContext2D.prototype.circle = function(x, y, r) { + this.beginPath(); + this.arc(x, y, r, 0, 2*Math.PI, false); + }; + + /** + * Draw a square shape + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r size, width and height of the square + */ + CanvasRenderingContext2D.prototype.square = function(x, y, r) { + this.beginPath(); + this.rect(x - r, y - r, r * 2, r * 2); + }; + + /** + * Draw a triangle shape + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r radius, half the length of the sides of the triangle + */ + CanvasRenderingContext2D.prototype.triangle = function(x, y, r) { + // http://en.wikipedia.org/wiki/Equilateral_triangle + this.beginPath(); + + var s = r * 2; + var s2 = s / 2; + var ir = Math.sqrt(3) / 6 * s; // radius of inner circle + var h = Math.sqrt(s * s - s2 * s2); // height + + this.moveTo(x, y - (h - ir)); + this.lineTo(x + s2, y + ir); + this.lineTo(x - s2, y + ir); + this.lineTo(x, y - (h - ir)); + this.closePath(); + }; + + /** + * Draw a triangle shape in downward orientation + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r radius + */ + CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) { + // http://en.wikipedia.org/wiki/Equilateral_triangle + this.beginPath(); + + var s = r * 2; + var s2 = s / 2; + var ir = Math.sqrt(3) / 6 * s; // radius of inner circle + var h = Math.sqrt(s * s - s2 * s2); // height + + this.moveTo(x, y + (h - ir)); + this.lineTo(x + s2, y - ir); + this.lineTo(x - s2, y - ir); + this.lineTo(x, y + (h - ir)); + this.closePath(); + }; + + /** + * Draw a star shape, a star with 5 points + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r radius, half the length of the sides of the triangle + */ + CanvasRenderingContext2D.prototype.star = function(x, y, r) { + // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/ + this.beginPath(); + + for (var n = 0; n < 10; n++) { + var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5; + this.lineTo( + x + radius * Math.sin(n * 2 * Math.PI / 10), + y - radius * Math.cos(n * 2 * Math.PI / 10) + ); + } + + this.closePath(); + }; + + /** + * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas + */ + CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) { + var r2d = Math.PI/180; + if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x + if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y + this.beginPath(); + this.moveTo(x+r,y); + this.lineTo(x+w-r,y); + this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false); + this.lineTo(x+w,y+h-r); + this.arc(x+w-r,y+h-r,r,0,r2d*90,false); + this.lineTo(x+r,y+h); + this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false); + this.lineTo(x,y+r); + this.arc(x+r,y+r,r,r2d*180,r2d*270,false); + }; + + /** + * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas + */ + CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) { + var kappa = .5522848, + ox = (w / 2) * kappa, // control point offset horizontal + oy = (h / 2) * kappa, // control point offset vertical + xe = x + w, // x-end + ye = y + h, // y-end + xm = x + w / 2, // x-middle + ym = y + h / 2; // y-middle + + this.beginPath(); + this.moveTo(x, ym); + this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + }; + + + + /** + * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas + */ + CanvasRenderingContext2D.prototype.database = function(x, y, w, h) { + var f = 1/3; + var wEllipse = w; + var hEllipse = h * f; + + var kappa = .5522848, + ox = (wEllipse / 2) * kappa, // control point offset horizontal + oy = (hEllipse / 2) * kappa, // control point offset vertical + xe = x + wEllipse, // x-end + ye = y + hEllipse, // y-end + xm = x + wEllipse / 2, // x-middle + ym = y + hEllipse / 2, // y-middle + ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse + yeb = y + h; // y-end, bottom ellipse + + this.beginPath(); + this.moveTo(xe, ym); + + this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + + this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + + this.lineTo(xe, ymb); + + this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb); + this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb); + + this.lineTo(x, ym); + }; + + + /** + * Draw an arrow point (no line) + */ + CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) { + // tail + var xt = x - length * Math.cos(angle); + var yt = y - length * Math.sin(angle); + + // inner tail + // TODO: allow to customize different shapes + var xi = x - length * 0.9 * Math.cos(angle); + var yi = y - length * 0.9 * Math.sin(angle); + + // left + var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI); + var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI); + + // right + var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI); + var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI); + + this.beginPath(); + this.moveTo(x, y); + this.lineTo(xl, yl); + this.lineTo(xi, yi); + this.lineTo(xr, yr); + this.closePath(); + }; + + /** + * Sets up the dashedLine functionality for drawing + * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas + * @author David Jordan + * @date 2012-08-08 + */ + CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){ + if (!dashArray) dashArray=[10,5]; + if (dashLength==0) dashLength = 0.001; // Hack for Safari + var dashCount = dashArray.length; + this.moveTo(x, y); + var dx = (x2-x), dy = (y2-y); + var slope = dy/dx; + var distRemaining = Math.sqrt( dx*dx + dy*dy ); + var dashIndex=0, draw=true; + while (distRemaining>=0.1){ + var dashLength = dashArray[dashIndex++%dashCount]; + if (dashLength > distRemaining) dashLength = distRemaining; + var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) ); + if (dx<0) xStep = -xStep; + x += xStep; + y += slope*xStep; + this[draw ? 'lineTo' : 'moveTo'](x,y); + distRemaining -= dashLength; + draw = !draw; + } + }; + + // TODO: add diamond shape } diff --git a/src/module/exports.js b/src/module/exports.js index da62569f..e4c70cbf 100644 --- a/src/module/exports.js +++ b/src/module/exports.js @@ -2,67 +2,67 @@ * vis.js module exports */ var vis = { - util: util, - events: events, + util: util, + events: events, - Controller: Controller, - DataSet: DataSet, - DataView: DataView, - Range: Range, - Stack: Stack, - TimeStep: TimeStep, - EventBus: EventBus, + Controller: Controller, + DataSet: DataSet, + DataView: DataView, + Range: Range, + Stack: Stack, + TimeStep: TimeStep, + EventBus: EventBus, - components: { - items: { - Item: Item, - ItemBox: ItemBox, - ItemPoint: ItemPoint, - ItemRange: ItemRange - }, - - Component: Component, - Panel: Panel, - RootPanel: RootPanel, - ItemSet: ItemSet, - TimeAxis: TimeAxis + components: { + items: { + Item: Item, + ItemBox: ItemBox, + ItemPoint: ItemPoint, + ItemRange: ItemRange }, - graph: { - Node: Node, - Edge: Edge, - Popup: Popup, - Groups: Groups, - Images: Images - }, + Component: Component, + Panel: Panel, + RootPanel: RootPanel, + ItemSet: ItemSet, + TimeAxis: TimeAxis + }, + + graph: { + Node: Node, + Edge: Edge, + Popup: Popup, + Groups: Groups, + Images: Images + }, - Timeline: Timeline, - Graph: Graph + Timeline: Timeline, + Graph: Graph }; /** * CommonJS module exports */ if (typeof exports !== 'undefined') { - exports = vis; + exports = vis; } if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { - module.exports = vis; + module.exports = vis; } /** * AMD module exports */ if (typeof(define) === 'function') { - define(function () { - return vis; - }); + define(function () { + return vis; + }); } /** * Window exports */ if (typeof window !== 'undefined') { - // attach the module to the window, load as a regular javascript file - window['vis'] = vis; + // attach the module to the window, load as a regular javascript file + window['vis'] = vis; } diff --git a/src/shim.js b/src/shim.js index 3bb50d55..b7b71691 100644 --- a/src/shim.js +++ b/src/shim.js @@ -3,31 +3,31 @@ // it here in that case. // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/ if(!Array.prototype.indexOf) { - Array.prototype.indexOf = function(obj){ - for(var i = 0; i < this.length; i++){ - if(this[i] == obj){ - return i; - } - } - return -1; - }; - - try { - console.log("Warning: Ancient browser detected. Please update your browser"); - } - catch (err) { + Array.prototype.indexOf = function(obj){ + for(var i = 0; i < this.length; i++){ + if(this[i] == obj){ + return i; + } } + return -1; + }; + + try { + console.log("Warning: Ancient browser detected. Please update your browser"); + } + catch (err) { + } } // Internet Explorer 8 and older does not support Array.forEach, so we define // it here in that case. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach if (!Array.prototype.forEach) { - Array.prototype.forEach = function(fn, scope) { - for(var i = 0, len = this.length; i < len; ++i) { - fn.call(scope || this, this[i], i, this); - } + Array.prototype.forEach = function(fn, scope) { + for(var i = 0, len = this.length; i < len; ++i) { + fn.call(scope || this, this[i], i, this); } + } } // Internet Explorer 8 and older does not support Array.map, so we define it @@ -36,106 +36,106 @@ if (!Array.prototype.forEach) { // Production steps of ECMA-262, Edition 5, 15.4.4.19 // Reference: http://es5.github.com/#x15.4.4.19 if (!Array.prototype.map) { - Array.prototype.map = function(callback, thisArg) { + Array.prototype.map = function(callback, thisArg) { - var T, A, k; + var T, A, k; - if (this == null) { - throw new TypeError(" this is null or not defined"); - } + if (this == null) { + throw new TypeError(" this is null or not defined"); + } - // 1. Let O be the result of calling ToObject passing the |this| value as the argument. - var O = Object(this); + // 1. Let O be the result of calling ToObject passing the |this| value as the argument. + var O = Object(this); - // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length". - // 3. Let len be ToUint32(lenValue). - var len = O.length >>> 0; + // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length". + // 3. Let len be ToUint32(lenValue). + var len = O.length >>> 0; - // 4. If IsCallable(callback) is false, throw a TypeError exception. - // See: http://es5.github.com/#x9.11 - if (typeof callback !== "function") { - throw new TypeError(callback + " is not a function"); - } + // 4. If IsCallable(callback) is false, throw a TypeError exception. + // See: http://es5.github.com/#x9.11 + if (typeof callback !== "function") { + throw new TypeError(callback + " is not a function"); + } - // 5. If thisArg was supplied, let T be thisArg; else let T be undefined. - if (thisArg) { - T = thisArg; - } + // 5. If thisArg was supplied, let T be thisArg; else let T be undefined. + if (thisArg) { + T = thisArg; + } - // 6. Let A be a new array created as if by the expression new Array(len) where Array is - // the standard built-in constructor with that name and len is the value of len. - A = new Array(len); + // 6. Let A be a new array created as if by the expression new Array(len) where Array is + // the standard built-in constructor with that name and len is the value of len. + A = new Array(len); - // 7. Let k be 0 - k = 0; + // 7. Let k be 0 + k = 0; - // 8. Repeat, while k < len - while(k < len) { + // 8. Repeat, while k < len + while(k < len) { - var kValue, mappedValue; + var kValue, mappedValue; - // a. Let Pk be ToString(k). - // This is implicit for LHS operands of the in operator - // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk. - // This step can be combined with c - // c. If kPresent is true, then - if (k in O) { + // a. Let Pk be ToString(k). + // This is implicit for LHS operands of the in operator + // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk. + // This step can be combined with c + // c. If kPresent is true, then + if (k in O) { - // i. Let kValue be the result of calling the Get internal method of O with argument Pk. - kValue = O[ k ]; + // i. Let kValue be the result of calling the Get internal method of O with argument Pk. + kValue = O[ k ]; - // ii. Let mappedValue be the result of calling the Call internal method of callback - // with T as the this value and argument list containing kValue, k, and O. - mappedValue = callback.call(T, kValue, k, O); + // ii. Let mappedValue be the result of calling the Call internal method of callback + // with T as the this value and argument list containing kValue, k, and O. + mappedValue = callback.call(T, kValue, k, O); - // iii. Call the DefineOwnProperty internal method of A with arguments - // Pk, Property Descriptor {Value: mappedValue, : true, Enumerable: true, Configurable: true}, - // and false. + // iii. Call the DefineOwnProperty internal method of A with arguments + // Pk, Property Descriptor {Value: mappedValue, : true, Enumerable: true, Configurable: true}, + // and false. - // In browsers that support Object.defineProperty, use the following: - // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true }); + // In browsers that support Object.defineProperty, use the following: + // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true }); - // For best browser support, use the following: - A[ k ] = mappedValue; - } - // d. Increase k by 1. - k++; - } + // For best browser support, use the following: + A[ k ] = mappedValue; + } + // d. Increase k by 1. + k++; + } - // 9. return A - return A; - }; + // 9. return A + return A; + }; } // Internet Explorer 8 and older does not support Array.filter, so we define it // here in that case. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/filter if (!Array.prototype.filter) { - Array.prototype.filter = function(fun /*, thisp */) { - "use strict"; + Array.prototype.filter = function(fun /*, thisp */) { + "use strict"; - if (this == null) { - throw new TypeError(); - } + if (this == null) { + throw new TypeError(); + } - var t = Object(this); - var len = t.length >>> 0; - if (typeof fun != "function") { - throw new TypeError(); - } + var t = Object(this); + var len = t.length >>> 0; + if (typeof fun != "function") { + throw new TypeError(); + } - var res = []; - var thisp = arguments[1]; - for (var i = 0; i < len; i++) { - if (i in t) { - var val = t[i]; // in case fun mutates this - if (fun.call(thisp, val, i, t)) - res.push(val); - } - } + var res = []; + var thisp = arguments[1]; + for (var i = 0; i < len; i++) { + if (i in t) { + var val = t[i]; // in case fun mutates this + if (fun.call(thisp, val, i, t)) + res.push(val); + } + } - return res; - }; + return res; + }; } @@ -143,110 +143,110 @@ if (!Array.prototype.filter) { // here in that case. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/keys if (!Object.keys) { - Object.keys = (function () { - var hasOwnProperty = Object.prototype.hasOwnProperty, - hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'), - dontEnums = [ - 'toString', - 'toLocaleString', - 'valueOf', - 'hasOwnProperty', - 'isPrototypeOf', - 'propertyIsEnumerable', - 'constructor' - ], - dontEnumsLength = dontEnums.length; - - return function (obj) { - if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) { - throw new TypeError('Object.keys called on non-object'); - } - - var result = []; - - for (var prop in obj) { - if (hasOwnProperty.call(obj, prop)) result.push(prop); - } - - if (hasDontEnumBug) { - for (var i=0; i < dontEnumsLength; i++) { - if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]); - } - } - return result; + Object.keys = (function () { + var hasOwnProperty = Object.prototype.hasOwnProperty, + hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'), + dontEnums = [ + 'toString', + 'toLocaleString', + 'valueOf', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'constructor' + ], + dontEnumsLength = dontEnums.length; + + return function (obj) { + if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) { + throw new TypeError('Object.keys called on non-object'); + } + + var result = []; + + for (var prop in obj) { + if (hasOwnProperty.call(obj, prop)) result.push(prop); + } + + if (hasDontEnumBug) { + for (var i=0; i < dontEnumsLength; i++) { + if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]); } - })() + } + return result; + } + })() } // Internet Explorer 8 and older does not support Array.isArray, // so we define it here in that case. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/isArray if(!Array.isArray) { - Array.isArray = function (vArg) { - return Object.prototype.toString.call(vArg) === "[object Array]"; - }; + Array.isArray = function (vArg) { + return Object.prototype.toString.call(vArg) === "[object Array]"; + }; } // Internet Explorer 8 and older does not support Function.bind, // so we define it here in that case. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind if (!Function.prototype.bind) { - Function.prototype.bind = function (oThis) { - if (typeof this !== "function") { - // closest thing possible to the ECMAScript 5 internal IsCallable function - throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); - } + Function.prototype.bind = function (oThis) { + if (typeof this !== "function") { + // closest thing possible to the ECMAScript 5 internal IsCallable function + throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); + } - var aArgs = Array.prototype.slice.call(arguments, 1), - fToBind = this, - fNOP = function () {}, - fBound = function () { - return fToBind.apply(this instanceof fNOP && oThis - ? this - : oThis, - aArgs.concat(Array.prototype.slice.call(arguments))); - }; - - fNOP.prototype = this.prototype; - fBound.prototype = new fNOP(); - - return fBound; - }; + var aArgs = Array.prototype.slice.call(arguments, 1), + fToBind = this, + fNOP = function () {}, + fBound = function () { + return fToBind.apply(this instanceof fNOP && oThis + ? this + : oThis, + aArgs.concat(Array.prototype.slice.call(arguments))); + }; + + fNOP.prototype = this.prototype; + fBound.prototype = new fNOP(); + + return fBound; + }; } // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create if (!Object.create) { - Object.create = function (o) { - if (arguments.length > 1) { - throw new Error('Object.create implementation only accepts the first parameter.'); - } - function F() {} - F.prototype = o; - return new F(); - }; + Object.create = function (o) { + if (arguments.length > 1) { + throw new Error('Object.create implementation only accepts the first parameter.'); + } + function F() {} + F.prototype = o; + return new F(); + }; } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind if (!Function.prototype.bind) { - Function.prototype.bind = function (oThis) { - if (typeof this !== "function") { - // closest thing possible to the ECMAScript 5 internal IsCallable function - throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); - } + Function.prototype.bind = function (oThis) { + if (typeof this !== "function") { + // closest thing possible to the ECMAScript 5 internal IsCallable function + throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); + } - var aArgs = Array.prototype.slice.call(arguments, 1), - fToBind = this, - fNOP = function () {}, - fBound = function () { - return fToBind.apply(this instanceof fNOP && oThis - ? this - : oThis, - aArgs.concat(Array.prototype.slice.call(arguments))); - }; - - fNOP.prototype = this.prototype; - fBound.prototype = new fNOP(); - - return fBound; - }; + var aArgs = Array.prototype.slice.call(arguments, 1), + fToBind = this, + fNOP = function () {}, + fBound = function () { + return fToBind.apply(this instanceof fNOP && oThis + ? this + : oThis, + aArgs.concat(Array.prototype.slice.call(arguments))); + }; + + fNOP.prototype = this.prototype; + fBound.prototype = new fNOP(); + + return fBound; + }; } diff --git a/src/timeline/Controller.js b/src/timeline/Controller.js index 7ec69a63..185d341b 100644 --- a/src/timeline/Controller.js +++ b/src/timeline/Controller.js @@ -4,11 +4,11 @@ * A Controller controls the reflows and repaints of all visual components */ function Controller () { - this.id = util.randomUUID(); - this.components = {}; + this.id = util.randomUUID(); + this.components = {}; - this.repaintTimer = undefined; - this.reflowTimer = undefined; + this.repaintTimer = undefined; + this.reflowTimer = undefined; } /** @@ -16,18 +16,18 @@ function Controller () { * @param {Component} component */ Controller.prototype.add = function add(component) { - // validate the component - if (component.id == undefined) { - throw new Error('Component has no field id'); - } - if (!(component instanceof Component) && !(component instanceof Controller)) { - throw new TypeError('Component must be an instance of ' + - 'prototype Component or Controller'); - } - - // add the component - component.controller = this; - this.components[component.id] = component; + // validate the component + if (component.id == undefined) { + throw new Error('Component has no field id'); + } + if (!(component instanceof Component) && !(component instanceof Controller)) { + throw new TypeError('Component must be an instance of ' + + 'prototype Component or Controller'); + } + + // add the component + component.controller = this; + this.components[component.id] = component; }; /** @@ -35,18 +35,18 @@ Controller.prototype.add = function add(component) { * @param {Component | String} component */ Controller.prototype.remove = function remove(component) { - var id; - for (id in this.components) { - if (this.components.hasOwnProperty(id)) { - if (id == component || this.components[id] == component) { - break; - } - } + var id; + for (id in this.components) { + if (this.components.hasOwnProperty(id)) { + if (id == component || this.components[id] == component) { + break; + } } + } - if (id) { - delete this.components[id]; - } + if (id) { + delete this.components[id]; + } }; /** @@ -55,18 +55,18 @@ Controller.prototype.remove = function remove(component) { * is false. */ Controller.prototype.requestReflow = function requestReflow(force) { - if (force) { - this.reflow(); - } - else { - if (!this.reflowTimer) { - var me = this; - this.reflowTimer = setTimeout(function () { - me.reflowTimer = undefined; - me.reflow(); - }, 0); - } + if (force) { + this.reflow(); + } + else { + if (!this.reflowTimer) { + var me = this; + this.reflowTimer = setTimeout(function () { + me.reflowTimer = undefined; + me.reflow(); + }, 0); } + } }; /** @@ -75,98 +75,98 @@ Controller.prototype.requestReflow = function requestReflow(force) { * is false. */ Controller.prototype.requestRepaint = function requestRepaint(force) { - if (force) { - this.repaint(); - } - else { - if (!this.repaintTimer) { - var me = this; - this.repaintTimer = setTimeout(function () { - me.repaintTimer = undefined; - me.repaint(); - }, 0); - } + if (force) { + this.repaint(); + } + else { + if (!this.repaintTimer) { + var me = this; + this.repaintTimer = setTimeout(function () { + me.repaintTimer = undefined; + me.repaint(); + }, 0); } + } }; /** * Repaint all components */ Controller.prototype.repaint = function repaint() { - var changed = false; + var changed = false; - // cancel any running repaint request - if (this.repaintTimer) { - clearTimeout(this.repaintTimer); - this.repaintTimer = undefined; - } - - var done = {}; - - function repaint(component, id) { - if (!(id in done)) { - // first repaint the components on which this component is dependent - if (component.depends) { - component.depends.forEach(function (dep) { - repaint(dep, dep.id); - }); - } - if (component.parent) { - repaint(component.parent, component.parent.id); - } - - // repaint the component itself and mark as done - changed = component.repaint() || changed; - done[id] = true; - } + // cancel any running repaint request + if (this.repaintTimer) { + clearTimeout(this.repaintTimer); + this.repaintTimer = undefined; + } + + var done = {}; + + function repaint(component, id) { + if (!(id in done)) { + // first repaint the components on which this component is dependent + if (component.depends) { + component.depends.forEach(function (dep) { + repaint(dep, dep.id); + }); + } + if (component.parent) { + repaint(component.parent, component.parent.id); + } + + // repaint the component itself and mark as done + changed = component.repaint() || changed; + done[id] = true; } + } - util.forEach(this.components, repaint); + util.forEach(this.components, repaint); - // immediately reflow when needed - if (changed) { - this.reflow(); - } - // TODO: limit the number of nested reflows/repaints, prevent loop + // immediately reflow when needed + if (changed) { + this.reflow(); + } + // TODO: limit the number of nested reflows/repaints, prevent loop }; /** * Reflow all components */ Controller.prototype.reflow = function reflow() { - var resized = false; + var resized = false; - // cancel any running repaint request - if (this.reflowTimer) { - clearTimeout(this.reflowTimer); - this.reflowTimer = undefined; - } - - var done = {}; - - function reflow(component, id) { - if (!(id in done)) { - // first reflow the components on which this component is dependent - if (component.depends) { - component.depends.forEach(function (dep) { - reflow(dep, dep.id); - }); - } - if (component.parent) { - reflow(component.parent, component.parent.id); - } - - // reflow the component itself and mark as done - resized = component.reflow() || resized; - done[id] = true; - } + // cancel any running repaint request + if (this.reflowTimer) { + clearTimeout(this.reflowTimer); + this.reflowTimer = undefined; + } + + var done = {}; + + function reflow(component, id) { + if (!(id in done)) { + // first reflow the components on which this component is dependent + if (component.depends) { + component.depends.forEach(function (dep) { + reflow(dep, dep.id); + }); + } + if (component.parent) { + reflow(component.parent, component.parent.id); + } + + // reflow the component itself and mark as done + resized = component.reflow() || resized; + done[id] = true; } + } - util.forEach(this.components, reflow); + util.forEach(this.components, reflow); - // immediately repaint when needed - if (resized) { - this.repaint(); - } - // TODO: limit the number of nested reflows/repaints, prevent loop + // immediately repaint when needed + if (resized) { + this.repaint(); + } + // TODO: limit the number of nested reflows/repaints, prevent loop }; diff --git a/src/timeline/Range.js b/src/timeline/Range.js index 09a943dd..1ebe7a45 100644 --- a/src/timeline/Range.js +++ b/src/timeline/Range.js @@ -7,13 +7,13 @@ * @extends Controller */ function Range(options) { - this.id = util.randomUUID(); - this.start = null; // Number - this.end = null; // Number + this.id = util.randomUUID(); + this.start = null; // Number + this.end = null; // Number - this.options = options || {}; + this.options = options || {}; - this.setOptions(options); + this.setOptions(options); } /** @@ -27,12 +27,12 @@ function Range(options) { * (end - start). */ Range.prototype.setOptions = function (options) { - util.extend(this.options, options); + util.extend(this.options, options); - // re-apply range with new limitations - if (this.start !== null && this.end !== null) { - this.setRange(this.start, this.end); - } + // re-apply range with new limitations + if (this.start !== null && this.end !== null) { + this.setRange(this.start, this.end); + } }; /** @@ -40,10 +40,10 @@ Range.prototype.setOptions = function (options) { * @param {String} direction 'horizontal' or 'vertical' */ function validateDirection (direction) { - if (direction != 'horizontal' && direction != 'vertical') { - throw new TypeError('Unknown direction "' + direction + '". ' + - 'Choose "horizontal" or "vertical".'); - } + if (direction != 'horizontal' && direction != 'vertical') { + throw new TypeError('Unknown direction "' + direction + '". ' + + 'Choose "horizontal" or "vertical".'); + } } /** @@ -53,44 +53,44 @@ function validateDirection (direction) { * @param {String} direction Available directions: 'horizontal', 'vertical' */ Range.prototype.subscribe = function (component, event, direction) { - var me = this; - - if (event == 'move') { - // drag start listener - component.on('dragstart', function (event) { - me._onDragStart(event, component); - }); - - // drag listener - component.on('drag', function (event) { - me._onDrag(event, component, direction); - }); - - // drag end listener - component.on('dragend', function (event) { - me._onDragEnd(event, component); - }); - } - else if (event == 'zoom') { - // mouse wheel - function mousewheel (event) { - me._onMouseWheel(event, component, direction); - } - component.on('mousewheel', mousewheel); - component.on('DOMMouseScroll', mousewheel); // For FF - - // pinch - component.on('touch', function (event) { - me._onTouch(); - }); - component.on('pinch', function (event) { - me._onPinch(event, component, direction); - }); - } - else { - throw new TypeError('Unknown event "' + event + '". ' + - 'Choose "move" or "zoom".'); + var me = this; + + if (event == 'move') { + // drag start listener + component.on('dragstart', function (event) { + me._onDragStart(event, component); + }); + + // drag listener + component.on('drag', function (event) { + me._onDrag(event, component, direction); + }); + + // drag end listener + component.on('dragend', function (event) { + me._onDragEnd(event, component); + }); + } + else if (event == 'zoom') { + // mouse wheel + function mousewheel (event) { + me._onMouseWheel(event, component, direction); } + component.on('mousewheel', mousewheel); + component.on('DOMMouseScroll', mousewheel); // For FF + + // pinch + component.on('touch', function (event) { + me._onTouch(); + }); + component.on('pinch', function (event) { + me._onPinch(event, component, direction); + }); + } + else { + throw new TypeError('Unknown event "' + event + '". ' + + 'Choose "move" or "zoom".'); + } }; /** @@ -100,7 +100,7 @@ Range.prototype.subscribe = function (component, event, direction) { * as parameter. */ Range.prototype.on = function (event, callback) { - events.addListener(this, event, callback); + events.addListener(this, event, callback); }; /** @@ -110,10 +110,10 @@ Range.prototype.on = function (event, callback) { * @private */ Range.prototype._trigger = function (event) { - events.trigger(this, event, { - start: this.start, - end: this.end - }); + events.trigger(this, event, { + start: this.start, + end: this.end + }); }; /** @@ -122,11 +122,11 @@ Range.prototype._trigger = function (event) { * @param {Number} [end] */ Range.prototype.setRange = function(start, end) { - var changed = this._applyRange(start, end); - if (changed) { - this._trigger('rangechange'); - this._trigger('rangechanged'); - } + var changed = this._applyRange(start, end); + if (changed) { + this._trigger('rangechange'); + this._trigger('rangechanged'); + } }; /** @@ -139,105 +139,105 @@ Range.prototype.setRange = function(start, end) { * @private */ Range.prototype._applyRange = function(start, end) { - var newStart = (start != null) ? util.convert(start, 'Number') : this.start, - newEnd = (end != null) ? util.convert(end, 'Number') : this.end, - max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null, - min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null, - diff; - - // check for valid number - if (isNaN(newStart) || newStart === null) { - throw new Error('Invalid start "' + start + '"'); - } - if (isNaN(newEnd) || newEnd === null) { - throw new Error('Invalid end "' + end + '"'); + var newStart = (start != null) ? util.convert(start, 'Number') : this.start, + newEnd = (end != null) ? util.convert(end, 'Number') : this.end, + max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null, + min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null, + diff; + + // check for valid number + if (isNaN(newStart) || newStart === null) { + throw new Error('Invalid start "' + start + '"'); + } + if (isNaN(newEnd) || newEnd === null) { + throw new Error('Invalid end "' + end + '"'); + } + + // prevent start < end + if (newEnd < newStart) { + newEnd = newStart; + } + + // prevent start < min + if (min !== null) { + if (newStart < min) { + diff = (min - newStart); + newStart += diff; + newEnd += diff; + + // prevent end > max + if (max != null) { + if (newEnd > max) { + newEnd = max; + } + } } + } - // prevent start < end - if (newEnd < newStart) { - newEnd = newStart; - } + // prevent end > max + if (max !== null) { + if (newEnd > max) { + diff = (newEnd - max); + newStart -= diff; + newEnd -= diff; - // prevent start < min - if (min !== null) { + // prevent start < min + if (min != null) { if (newStart < min) { - diff = (min - newStart); - newStart += diff; - newEnd += diff; - - // prevent end > max - if (max != null) { - if (newEnd > max) { - newEnd = max; - } - } + newStart = min; } + } } + } - // prevent end > max - if (max !== null) { - if (newEnd > max) { - diff = (newEnd - max); - newStart -= diff; - newEnd -= diff; - - // prevent start < min - if (min != null) { - if (newStart < min) { - newStart = min; - } - } - } + // prevent (end-start) < zoomMin + if (this.options.zoomMin !== null) { + var zoomMin = parseFloat(this.options.zoomMin); + if (zoomMin < 0) { + zoomMin = 0; } - - // prevent (end-start) < zoomMin - if (this.options.zoomMin !== null) { - var zoomMin = parseFloat(this.options.zoomMin); - if (zoomMin < 0) { - zoomMin = 0; - } - if ((newEnd - newStart) < zoomMin) { - if ((this.end - this.start) === zoomMin) { - // ignore this action, we are already zoomed to the minimum - newStart = this.start; - newEnd = this.end; - } - else { - // zoom to the minimum - diff = (zoomMin - (newEnd - newStart)); - newStart -= diff / 2; - newEnd += diff / 2; - } - } + if ((newEnd - newStart) < zoomMin) { + if ((this.end - this.start) === zoomMin) { + // ignore this action, we are already zoomed to the minimum + newStart = this.start; + newEnd = this.end; + } + else { + // zoom to the minimum + diff = (zoomMin - (newEnd - newStart)); + newStart -= diff / 2; + newEnd += diff / 2; + } } + } - // prevent (end-start) > zoomMax - if (this.options.zoomMax !== null) { - var zoomMax = parseFloat(this.options.zoomMax); - if (zoomMax < 0) { - zoomMax = 0; - } - if ((newEnd - newStart) > zoomMax) { - if ((this.end - this.start) === zoomMax) { - // ignore this action, we are already zoomed to the maximum - newStart = this.start; - newEnd = this.end; - } - else { - // zoom to the maximum - diff = ((newEnd - newStart) - zoomMax); - newStart += diff / 2; - newEnd -= diff / 2; - } - } + // prevent (end-start) > zoomMax + if (this.options.zoomMax !== null) { + var zoomMax = parseFloat(this.options.zoomMax); + if (zoomMax < 0) { + zoomMax = 0; } + if ((newEnd - newStart) > zoomMax) { + if ((this.end - this.start) === zoomMax) { + // ignore this action, we are already zoomed to the maximum + newStart = this.start; + newEnd = this.end; + } + else { + // zoom to the maximum + diff = ((newEnd - newStart) - zoomMax); + newStart += diff / 2; + newEnd -= diff / 2; + } + } + } - var changed = (this.start != newStart || this.end != newEnd); + var changed = (this.start != newStart || this.end != newEnd); - this.start = newStart; - this.end = newEnd; + this.start = newStart; + this.end = newEnd; - return changed; + return changed; }; /** @@ -245,10 +245,10 @@ Range.prototype._applyRange = function(start, end) { * @return {Object} An object with start and end properties */ Range.prototype.getRange = function() { - return { - start: this.start, - end: this.end - }; + return { + start: this.start, + end: this.end + }; }; /** @@ -258,7 +258,7 @@ Range.prototype.getRange = function() { * @returns {{offset: number, scale: number}} conversion */ Range.prototype.conversion = function (width) { - return Range.conversion(this.start, this.end, width); + return Range.conversion(this.start, this.end, width); }; /** @@ -270,18 +270,18 @@ Range.prototype.conversion = function (width) { * @returns {{offset: number, scale: number}} conversion */ Range.conversion = function (start, end, width) { - if (width != 0 && (end - start != 0)) { - return { - offset: start, - scale: width / (end - start) - } - } - else { - return { - offset: 0, - scale: 1 - }; + if (width != 0 && (end - start != 0)) { + return { + offset: start, + scale: width / (end - start) } + } + else { + return { + offset: 0, + scale: 1 + }; + } }; // global (private) object to store drag params @@ -294,17 +294,17 @@ var touchParams = {}; * @private */ Range.prototype._onDragStart = function(event, component) { - // refuse to drag when we where pinching to prevent the timeline make a jump - // when releasing the fingers in opposite order from the touch screen - if (touchParams.pinching) return; + // refuse to drag when we where pinching to prevent the timeline make a jump + // when releasing the fingers in opposite order from the touch screen + if (touchParams.pinching) return; - touchParams.start = this.start; - touchParams.end = this.end; + touchParams.start = this.start; + touchParams.end = this.end; - var frame = component.frame; - if (frame) { - frame.style.cursor = 'move'; - } + var frame = component.frame; + if (frame) { + frame.style.cursor = 'move'; + } }; /** @@ -315,21 +315,21 @@ Range.prototype._onDragStart = function(event, component) { * @private */ Range.prototype._onDrag = function (event, component, direction) { - validateDirection(direction); + validateDirection(direction); - // refuse to drag when we where pinching to prevent the timeline make a jump - // when releasing the fingers in opposite order from the touch screen - if (touchParams.pinching) return; + // refuse to drag when we where pinching to prevent the timeline make a jump + // when releasing the fingers in opposite order from the touch screen + if (touchParams.pinching) return; - var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY, - interval = (touchParams.end - touchParams.start), - width = (direction == 'horizontal') ? component.width : component.height, - diffRange = -delta / width * interval; + var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY, + interval = (touchParams.end - touchParams.start), + width = (direction == 'horizontal') ? component.width : component.height, + diffRange = -delta / width * interval; - this._applyRange(touchParams.start + diffRange, touchParams.end + diffRange); + this._applyRange(touchParams.start + diffRange, touchParams.end + diffRange); - // fire a rangechange event - this._trigger('rangechange'); + // fire a rangechange event + this._trigger('rangechange'); }; /** @@ -339,16 +339,16 @@ Range.prototype._onDrag = function (event, component, direction) { * @private */ Range.prototype._onDragEnd = function (event, component) { - // refuse to drag when we where pinching to prevent the timeline make a jump - // when releasing the fingers in opposite order from the touch screen - if (touchParams.pinching) return; + // refuse to drag when we where pinching to prevent the timeline make a jump + // when releasing the fingers in opposite order from the touch screen + if (touchParams.pinching) return; - if (component.frame) { - component.frame.style.cursor = 'auto'; - } + if (component.frame) { + component.frame.style.cursor = 'auto'; + } - // fire a rangechanged event - this._trigger('rangechanged'); + // fire a rangechanged event + this._trigger('rangechanged'); }; /** @@ -360,45 +360,45 @@ Range.prototype._onDragEnd = function (event, component) { * @private */ Range.prototype._onMouseWheel = function(event, component, direction) { - validateDirection(direction); - - // retrieve delta - var 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; + validateDirection(direction); + + // retrieve delta + var 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) { + // perform the zoom action. Delta is normally 1 or -1 + + // adjust a negative delta such that zooming in with delta 0.1 + // equals zooming out with a delta -0.1 + var scale; + if (delta < 0) { + scale = 1 - (delta / 5); + } + else { + scale = 1 / (1 + (delta / 5)) ; } - // 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) { - // perform the zoom action. Delta is normally 1 or -1 - - // adjust a negative delta such that zooming in with delta 0.1 - // equals zooming out with a delta -0.1 - var scale; - if (delta < 0) { - scale = 1 - (delta / 5); - } - else { - scale = 1 / (1 + (delta / 5)) ; - } - - // calculate center, the date to zoom around - var gesture = Hammer.event.collectEventData(this, 'scroll', event), - pointer = getPointer(gesture.touches[0], component.frame), - pointerDate = this._pointerToDate(component, direction, pointer); + // calculate center, the date to zoom around + var gesture = Hammer.event.collectEventData(this, 'scroll', event), + pointer = getPointer(gesture.touches[0], component.frame), + pointerDate = this._pointerToDate(component, direction, pointer); - this.zoom(scale, pointerDate); - } + this.zoom(scale, pointerDate); + } - // Prevent default actions caused by mouse wheel - // (else the page and timeline both zoom and scroll) - util.preventDefault(event); + // Prevent default actions caused by mouse wheel + // (else the page and timeline both zoom and scroll) + util.preventDefault(event); }; /** @@ -406,10 +406,10 @@ Range.prototype._onMouseWheel = function(event, component, direction) { * @private */ Range.prototype._onTouch = function () { - touchParams.start = this.start; - touchParams.end = this.end; - touchParams.pinching = false; - touchParams.center = null; + touchParams.start = this.start; + touchParams.end = this.end; + touchParams.pinching = false; + touchParams.center = null; }; /** @@ -420,26 +420,26 @@ Range.prototype._onTouch = function () { * @private */ Range.prototype._onPinch = function (event, component, direction) { - touchParams.pinching = true; + touchParams.pinching = true; - if (event.gesture.touches.length > 1) { - if (!touchParams.center) { - touchParams.center = getPointer(event.gesture.center, component.frame); - } + if (event.gesture.touches.length > 1) { + if (!touchParams.center) { + touchParams.center = getPointer(event.gesture.center, component.frame); + } - var scale = 1 / event.gesture.scale, - initDate = this._pointerToDate(component, direction, touchParams.center), - center = getPointer(event.gesture.center, component.frame), - date = this._pointerToDate(component, direction, center), - delta = date - initDate; // TODO: utilize delta + var scale = 1 / event.gesture.scale, + initDate = this._pointerToDate(component, direction, touchParams.center), + center = getPointer(event.gesture.center, component.frame), + date = this._pointerToDate(component, direction, center), + delta = date - initDate; // TODO: utilize delta - // calculate new start and end - var newStart = parseInt(initDate + (touchParams.start - initDate) * scale); - var newEnd = parseInt(initDate + (touchParams.end - initDate) * scale); + // calculate new start and end + var newStart = parseInt(initDate + (touchParams.start - initDate) * scale); + var newEnd = parseInt(initDate + (touchParams.end - initDate) * scale); - // apply new range - this.setRange(newStart, newEnd); - } + // apply new range + this.setRange(newStart, newEnd); + } }; /** @@ -451,17 +451,17 @@ Range.prototype._onPinch = function (event, component, direction) { * @private */ Range.prototype._pointerToDate = function (component, direction, pointer) { - var conversion; - if (direction == 'horizontal') { - var width = component.width; - conversion = this.conversion(width); - return pointer.x / conversion.scale + conversion.offset; - } - else { - var height = component.height; - conversion = this.conversion(height); - return pointer.y / conversion.scale + conversion.offset; - } + var conversion; + if (direction == 'horizontal') { + var width = component.width; + conversion = this.conversion(width); + return pointer.x / conversion.scale + conversion.offset; + } + else { + var height = component.height; + conversion = this.conversion(height); + return pointer.y / conversion.scale + conversion.offset; + } }; /** @@ -472,10 +472,10 @@ Range.prototype._pointerToDate = function (component, direction, pointer) { * @private */ function getPointer (touch, element) { - return { - x: touch.pageX - vis.util.getAbsoluteLeft(element), - y: touch.pageY - vis.util.getAbsoluteTop(element) - }; + return { + x: touch.pageX - vis.util.getAbsoluteLeft(element), + y: touch.pageY - vis.util.getAbsoluteTop(element) + }; } /** @@ -489,16 +489,16 @@ function getPointer (touch, element) { * be zoomed. */ Range.prototype.zoom = function(scale, center) { - // if centerDate is not provided, take it half between start Date and end Date - if (center == null) { - center = (this.start + this.end) / 2; - } + // if centerDate is not provided, take it half between start Date and end Date + if (center == null) { + center = (this.start + this.end) / 2; + } - // calculate new start and end - var newStart = center + (this.start - center) * scale; - var newEnd = center + (this.end - center) * scale; + // calculate new start and end + var newStart = center + (this.start - center) * scale; + var newEnd = center + (this.end - center) * scale; - this.setRange(newStart, newEnd); + this.setRange(newStart, newEnd); }; /** @@ -508,17 +508,17 @@ Range.prototype.zoom = function(scale, center) { * negative value will move left */ Range.prototype.move = function(delta) { - // zoom start Date and end Date relative to the centerDate - var diff = (this.end - this.start); + // zoom start Date and end Date relative to the centerDate + var diff = (this.end - this.start); - // apply new values - var newStart = this.start + diff * delta; - var newEnd = this.end + diff * delta; + // apply new values + var newStart = this.start + diff * delta; + var newEnd = this.end + diff * delta; - // TODO: reckon with min and max range + // TODO: reckon with min and max range - this.start = newStart; - this.end = newEnd; + this.start = newStart; + this.end = newEnd; }; /** @@ -526,13 +526,13 @@ Range.prototype.move = function(delta) { * @param {Number} moveTo New center point of the range */ Range.prototype.moveTo = function(moveTo) { - var center = (this.start + this.end) / 2; + var center = (this.start + this.end) / 2; - var diff = center - moveTo; + var diff = center - moveTo; - // calculate new start and end - var newStart = this.start - diff; - var newEnd = this.end - diff; + // calculate new start and end + var newStart = this.start - diff; + var newEnd = this.end - diff; - this.setRange(newStart, newEnd); + this.setRange(newStart, newEnd); }; diff --git a/src/timeline/Stack.js b/src/timeline/Stack.js index e714aee4..017c98ce 100644 --- a/src/timeline/Stack.js +++ b/src/timeline/Stack.js @@ -5,39 +5,39 @@ * @param {Object} [options] */ function Stack (parent, options) { - this.parent = parent; - - this.options = options || {}; - this.defaultOptions = { - order: function (a, b) { - //return (b.width - a.width) || (a.left - b.left); // TODO: cleanup - // Order: ranges over non-ranges, ranged ordered by width, and - // lastly ordered by start. - if (a instanceof ItemRange) { - if (b instanceof ItemRange) { - var aInt = (a.data.end - a.data.start); - var bInt = (b.data.end - b.data.start); - return (aInt - bInt) || (a.data.start - b.data.start); - } - else { - return -1; - } - } - else { - if (b instanceof ItemRange) { - return 1; - } - else { - return (a.data.start - b.data.start); - } - } - }, - margin: { - item: 10 + this.parent = parent; + + this.options = options || {}; + this.defaultOptions = { + order: function (a, b) { + //return (b.width - a.width) || (a.left - b.left); // TODO: cleanup + // Order: ranges over non-ranges, ranged ordered by width, and + // lastly ordered by start. + if (a instanceof ItemRange) { + if (b instanceof ItemRange) { + var aInt = (a.data.end - a.data.start); + var bInt = (b.data.end - b.data.start); + return (aInt - bInt) || (a.data.start - b.data.start); } - }; + else { + return -1; + } + } + else { + if (b instanceof ItemRange) { + return 1; + } + else { + return (a.data.start - b.data.start); + } + } + }, + margin: { + item: 10 + } + }; - this.ordered = []; // ordered items + this.ordered = []; // ordered items } /** @@ -48,9 +48,9 @@ function Stack (parent, options) { * {function} order Stacking order */ Stack.prototype.setOptions = function setOptions (options) { - util.extend(this.options, options); + util.extend(this.options, options); - // TODO: register on data changes at the connected parent itemset, and update the changed part only and immediately + // TODO: register on data changes at the connected parent itemset, and update the changed part only and immediately }; /** @@ -58,8 +58,8 @@ Stack.prototype.setOptions = function setOptions (options) { * distance equal to options.margin.item. */ Stack.prototype.update = function update() { - this._order(); - this._stack(); + this._order(); + this._stack(); }; /** @@ -70,31 +70,31 @@ Stack.prototype.update = function update() { * @private */ Stack.prototype._order = function _order () { - var items = this.parent.items; - if (!items) { - throw new Error('Cannot stack items: parent does not contain items'); + var items = this.parent.items; + if (!items) { + throw new Error('Cannot stack items: parent does not contain items'); + } + + // TODO: store the sorted items, to have less work later on + var ordered = []; + var index = 0; + // items is a map (no array) + util.forEach(items, function (item) { + if (item.visible) { + ordered[index] = item; + index++; } + }); - // TODO: store the sorted items, to have less work later on - var ordered = []; - var index = 0; - // items is a map (no array) - util.forEach(items, function (item) { - if (item.visible) { - ordered[index] = item; - index++; - } - }); - - //if a customer stack order function exists, use it. - var order = this.options.order || this.defaultOptions.order; - if (!(typeof order === 'function')) { - throw new Error('Option order must be a function'); - } + //if a customer stack order function exists, use it. + var order = this.options.order || this.defaultOptions.order; + if (!(typeof order === 'function')) { + throw new Error('Option order must be a function'); + } - ordered.sort(order); + ordered.sort(order); - this.ordered = ordered; + this.ordered = ordered; }; /** @@ -103,40 +103,40 @@ Stack.prototype._order = function _order () { * @private */ Stack.prototype._stack = function _stack () { - var i, - iMax, - ordered = this.ordered, - options = this.options, - orientation = options.orientation || this.defaultOptions.orientation, - axisOnTop = (orientation == 'top'), - margin; - - if (options.margin && options.margin.item !== undefined) { - margin = options.margin.item; - } - else { - margin = this.defaultOptions.margin.item - } - - // calculate new, non-overlapping positions - for (i = 0, iMax = ordered.length; i < iMax; i++) { - var item = ordered[i]; - var collidingItem = null; - do { - // TODO: optimize checking for overlap. when there is a gap without items, - // you only need to check for items from the next item on, not from zero - collidingItem = this.checkOverlap(ordered, i, 0, i - 1, margin); - if (collidingItem != null) { - // There is a collision. Reposition the event above the colliding element - if (axisOnTop) { - item.top = collidingItem.top + collidingItem.height + margin; - } - else { - item.top = collidingItem.top - item.height - margin; - } - } - } while (collidingItem); - } + var i, + iMax, + ordered = this.ordered, + options = this.options, + orientation = options.orientation || this.defaultOptions.orientation, + axisOnTop = (orientation == 'top'), + margin; + + if (options.margin && options.margin.item !== undefined) { + margin = options.margin.item; + } + else { + margin = this.defaultOptions.margin.item + } + + // calculate new, non-overlapping positions + for (i = 0, iMax = ordered.length; i < iMax; i++) { + var item = ordered[i]; + var collidingItem = null; + do { + // TODO: optimize checking for overlap. when there is a gap without items, + // you only need to check for items from the next item on, not from zero + collidingItem = this.checkOverlap(ordered, i, 0, i - 1, margin); + if (collidingItem != null) { + // There is a collision. Reposition the event above the colliding element + if (axisOnTop) { + item.top = collidingItem.top + collidingItem.height + margin; + } + else { + item.top = collidingItem.top - item.height - margin; + } + } + } while (collidingItem); + } }; /** @@ -155,21 +155,21 @@ Stack.prototype._stack = function _stack () { */ Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex, itemStart, itemEnd, margin) { - var collision = this.collision; - - // we loop from end to start, as we suppose that the chance of a - // collision is larger for items at the end, so check these first. - var a = items[itemIndex]; - for (var i = itemEnd; i >= itemStart; i--) { - var b = items[i]; - if (collision(a, b, margin)) { - if (i != itemIndex) { - return b; - } - } + var collision = this.collision; + + // we loop from end to start, as we suppose that the chance of a + // collision is larger for items at the end, so check these first. + var a = items[itemIndex]; + for (var i = itemEnd; i >= itemStart; i--) { + var b = items[i]; + if (collision(a, b, margin)) { + if (i != itemIndex) { + return b; + } } + } - return null; + return null; }; /** @@ -185,8 +185,8 @@ Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex, * @return {boolean} true if a and b collide, else false */ Stack.prototype.collision = function collision (a, b, margin) { - return ((a.left - margin) < (b.left + b.getWidth()) && - (a.left + a.getWidth() + margin) > b.left && - (a.top - margin) < (b.top + b.height) && - (a.top + a.height + margin) > b.top); + return ((a.left - margin) < (b.left + b.getWidth()) && + (a.left + a.getWidth() + margin) > b.left && + (a.top - margin) < (b.top + b.height) && + (a.top + a.height + margin) > b.top); }; diff --git a/src/timeline/TimeStep.js b/src/timeline/TimeStep.js index ccbc6c9f..fc56b518 100644 --- a/src/timeline/TimeStep.js +++ b/src/timeline/TimeStep.js @@ -25,29 +25,29 @@ * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds */ TimeStep = function(start, end, minimumStep) { - // variables - this.current = new Date(); - this._start = new Date(); - this._end = new Date(); + // variables + this.current = new Date(); + this._start = new Date(); + this._end = new Date(); - this.autoScale = true; - this.scale = TimeStep.SCALE.DAY; - this.step = 1; + this.autoScale = true; + this.scale = TimeStep.SCALE.DAY; + this.step = 1; - // initialize the range - this.setRange(start, end, minimumStep); + // initialize the range + this.setRange(start, end, minimumStep); }; /// enum scale TimeStep.SCALE = { - MILLISECOND: 1, - SECOND: 2, - MINUTE: 3, - HOUR: 4, - DAY: 5, - WEEKDAY: 6, - MONTH: 7, - YEAR: 8 + MILLISECOND: 1, + SECOND: 2, + MINUTE: 3, + HOUR: 4, + DAY: 5, + WEEKDAY: 6, + MONTH: 7, + YEAR: 8 }; @@ -62,24 +62,24 @@ TimeStep.SCALE = { * @param {int} [minimumStep] Optional. Minimum step size in milliseconds */ TimeStep.prototype.setRange = function(start, end, minimumStep) { - if (!(start instanceof Date) || !(end instanceof Date)) { - throw "No legal start or end date in method setRange"; - } + if (!(start instanceof Date) || !(end instanceof Date)) { + throw "No legal start or end date in method setRange"; + } - this._start = (start != undefined) ? new Date(start.valueOf()) : new Date(); - this._end = (end != undefined) ? new Date(end.valueOf()) : new Date(); + this._start = (start != undefined) ? new Date(start.valueOf()) : new Date(); + this._end = (end != undefined) ? new Date(end.valueOf()) : new Date(); - if (this.autoScale) { - this.setMinimumStep(minimumStep); - } + if (this.autoScale) { + this.setMinimumStep(minimumStep); + } }; /** * Set the range iterator to the start date. */ TimeStep.prototype.first = function() { - this.current = new Date(this._start.valueOf()); - this.roundToMinor(); + this.current = new Date(this._start.valueOf()); + this.roundToMinor(); }; /** @@ -87,36 +87,36 @@ TimeStep.prototype.first = function() { * This must be executed once when the current date is set to start Date */ TimeStep.prototype.roundToMinor = function() { - // round to floor - // IMPORTANT: we have no breaks in this switch! (this is no bug) - //noinspection FallthroughInSwitchStatementJS + // round to floor + // IMPORTANT: we have no breaks in this switch! (this is no bug) + //noinspection FallthroughInSwitchStatementJS + switch (this.scale) { + case TimeStep.SCALE.YEAR: + this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step)); + this.current.setMonth(0); + case TimeStep.SCALE.MONTH: this.current.setDate(1); + case TimeStep.SCALE.DAY: // intentional fall through + case TimeStep.SCALE.WEEKDAY: this.current.setHours(0); + case TimeStep.SCALE.HOUR: this.current.setMinutes(0); + case TimeStep.SCALE.MINUTE: this.current.setSeconds(0); + case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0); + //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds + } + + if (this.step != 1) { + // round down to the first minor value that is a multiple of the current step size switch (this.scale) { - case TimeStep.SCALE.YEAR: - this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step)); - this.current.setMonth(0); - case TimeStep.SCALE.MONTH: this.current.setDate(1); - case TimeStep.SCALE.DAY: // intentional fall through - case TimeStep.SCALE.WEEKDAY: this.current.setHours(0); - case TimeStep.SCALE.HOUR: this.current.setMinutes(0); - case TimeStep.SCALE.MINUTE: this.current.setSeconds(0); - case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0); - //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds - } - - if (this.step != 1) { - // round down to the first minor value that is a multiple of the current step size - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break; - case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break; - case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break; - case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break; - case TimeStep.SCALE.WEEKDAY: // intentional fall through - case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break; - case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break; - case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break; - default: break; - } + case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break; + case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break; + case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break; + case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break; + case TimeStep.SCALE.WEEKDAY: // intentional fall through + case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break; + case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break; + case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break; + default: break; } + } }; /** @@ -124,70 +124,70 @@ TimeStep.prototype.roundToMinor = function() { * @return {boolean} true if the current date has not passed the end date */ TimeStep.prototype.hasNext = function () { - return (this.current.valueOf() <= this._end.valueOf()); + return (this.current.valueOf() <= this._end.valueOf()); }; /** * Do the next step */ TimeStep.prototype.next = function() { - var prev = this.current.valueOf(); - - // Two cases, needed to prevent issues with switching daylight savings - // (end of March and end of October) - if (this.current.getMonth() < 6) { - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND: - - this.current = new Date(this.current.valueOf() + this.step); break; - case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break; - case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break; - case TimeStep.SCALE.HOUR: - this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60); - // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...) - var h = this.current.getHours(); - this.current.setHours(h - (h % this.step)); - break; - case TimeStep.SCALE.WEEKDAY: // intentional fall through - case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; - case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; - case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break; - default: break; - } + var prev = this.current.valueOf(); + + // Two cases, needed to prevent issues with switching daylight savings + // (end of March and end of October) + if (this.current.getMonth() < 6) { + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND: + + this.current = new Date(this.current.valueOf() + this.step); break; + case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break; + case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break; + case TimeStep.SCALE.HOUR: + this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60); + // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...) + var h = this.current.getHours(); + this.current.setHours(h - (h % this.step)); + break; + case TimeStep.SCALE.WEEKDAY: // intentional fall through + case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; + case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; + case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break; + default: break; } - else { - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break; - case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break; - case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break; - case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break; - case TimeStep.SCALE.WEEKDAY: // intentional fall through - case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; - case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; - case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break; - default: break; - } + } + else { + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break; + case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break; + case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break; + case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break; + case TimeStep.SCALE.WEEKDAY: // intentional fall through + case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; + case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; + case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break; + default: break; } + } - if (this.step != 1) { - // round down to the correct major value - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break; - case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break; - case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break; - case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break; - case TimeStep.SCALE.WEEKDAY: // intentional fall through - case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break; - case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break; - case TimeStep.SCALE.YEAR: break; // nothing to do for year - default: break; - } + if (this.step != 1) { + // round down to the correct major value + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break; + case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break; + case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break; + case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break; + case TimeStep.SCALE.WEEKDAY: // intentional fall through + case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break; + case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break; + case TimeStep.SCALE.YEAR: break; // nothing to do for year + default: break; } + } - // safety mechanism: if current time is still unchanged, move to the end - if (this.current.valueOf() == prev) { - this.current = new Date(this._end.valueOf()); - } + // safety mechanism: if current time is still unchanged, move to the end + if (this.current.valueOf() == prev) { + this.current = new Date(this._end.valueOf()); + } }; @@ -196,7 +196,7 @@ TimeStep.prototype.next = function() { * @return {Date} current The current date */ TimeStep.prototype.getCurrent = function() { - return this.current; + return this.current; }; /** @@ -213,13 +213,13 @@ TimeStep.prototype.getCurrent = function() { * example 1, 2, 5, or 10. */ TimeStep.prototype.setScale = function(newScale, newStep) { - this.scale = newScale; + this.scale = newScale; - if (newStep > 0) { - this.step = newStep; - } + if (newStep > 0) { + this.step = newStep; + } - this.autoScale = false; + this.autoScale = false; }; /** @@ -227,7 +227,7 @@ TimeStep.prototype.setScale = function(newScale, newStep) { * @param {boolean} enable If true, autoascaling is set true */ TimeStep.prototype.setAutoScale = function (enable) { - this.autoScale = enable; + this.autoScale = enable; }; @@ -236,48 +236,48 @@ TimeStep.prototype.setAutoScale = function (enable) { * @param {Number} [minimumStep] The minimum step size in milliseconds */ TimeStep.prototype.setMinimumStep = function(minimumStep) { - if (minimumStep == undefined) { - return; - } - - var stepYear = (1000 * 60 * 60 * 24 * 30 * 12); - var stepMonth = (1000 * 60 * 60 * 24 * 30); - var stepDay = (1000 * 60 * 60 * 24); - var stepHour = (1000 * 60 * 60); - var stepMinute = (1000 * 60); - var stepSecond = (1000); - var stepMillisecond= (1); - - // find the smallest step that is larger than the provided minimumStep - if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;} - if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;} - if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;} - if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;} - if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;} - if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;} - if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;} - if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;} - if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;} - if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;} - if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;} - if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;} - if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;} - if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;} - if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;} - if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;} - if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;} - if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;} - if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;} - if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;} - if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;} - if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;} - if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;} - if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;} - if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;} - if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;} - if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;} - if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;} - if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;} + if (minimumStep == undefined) { + return; + } + + var stepYear = (1000 * 60 * 60 * 24 * 30 * 12); + var stepMonth = (1000 * 60 * 60 * 24 * 30); + var stepDay = (1000 * 60 * 60 * 24); + var stepHour = (1000 * 60 * 60); + var stepMinute = (1000 * 60); + var stepSecond = (1000); + var stepMillisecond= (1); + + // find the smallest step that is larger than the provided minimumStep + if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;} + if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;} + if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;} + if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;} + if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;} + if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;} + if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;} + if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;} + if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;} + if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;} + if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;} + if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;} + if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;} + if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;} + if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;} + if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;} + if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;} + if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;} + if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;} + if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;} + if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;} + if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;} + if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;} + if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;} + if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;} + if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;} + if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;} + if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;} + if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;} }; /** @@ -286,87 +286,87 @@ TimeStep.prototype.setMinimumStep = function(minimumStep) { * @param {Date} date the date to be snapped */ TimeStep.prototype.snap = function(date) { - if (this.scale == TimeStep.SCALE.YEAR) { - var year = date.getFullYear() + Math.round(date.getMonth() / 12); - date.setFullYear(Math.round(year / this.step) * this.step); - date.setMonth(0); - date.setDate(0); - date.setHours(0); - date.setMinutes(0); - date.setSeconds(0); - date.setMilliseconds(0); + if (this.scale == TimeStep.SCALE.YEAR) { + var year = date.getFullYear() + Math.round(date.getMonth() / 12); + date.setFullYear(Math.round(year / this.step) * this.step); + date.setMonth(0); + date.setDate(0); + date.setHours(0); + date.setMinutes(0); + date.setSeconds(0); + date.setMilliseconds(0); + } + else if (this.scale == TimeStep.SCALE.MONTH) { + if (date.getDate() > 15) { + date.setDate(1); + date.setMonth(date.getMonth() + 1); + // important: first set Date to 1, after that change the month. } - else if (this.scale == TimeStep.SCALE.MONTH) { - if (date.getDate() > 15) { - date.setDate(1); - date.setMonth(date.getMonth() + 1); - // important: first set Date to 1, after that change the month. - } - else { - date.setDate(1); - } - - date.setHours(0); - date.setMinutes(0); - date.setSeconds(0); - date.setMilliseconds(0); + else { + date.setDate(1); } - else if (this.scale == TimeStep.SCALE.DAY || - this.scale == TimeStep.SCALE.WEEKDAY) { - //noinspection FallthroughInSwitchStatementJS - switch (this.step) { - case 5: - case 2: - date.setHours(Math.round(date.getHours() / 24) * 24); break; - default: - date.setHours(Math.round(date.getHours() / 12) * 12); break; - } - date.setMinutes(0); - date.setSeconds(0); - date.setMilliseconds(0); + + date.setHours(0); + date.setMinutes(0); + date.setSeconds(0); + date.setMilliseconds(0); + } + else if (this.scale == TimeStep.SCALE.DAY || + this.scale == TimeStep.SCALE.WEEKDAY) { + //noinspection FallthroughInSwitchStatementJS + switch (this.step) { + case 5: + case 2: + date.setHours(Math.round(date.getHours() / 24) * 24); break; + default: + date.setHours(Math.round(date.getHours() / 12) * 12); break; } - else if (this.scale == TimeStep.SCALE.HOUR) { - switch (this.step) { - case 4: - date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break; - default: - date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break; - } - date.setSeconds(0); - date.setMilliseconds(0); - } else if (this.scale == TimeStep.SCALE.MINUTE) { - //noinspection FallthroughInSwitchStatementJS - switch (this.step) { - case 15: - case 10: - date.setMinutes(Math.round(date.getMinutes() / 5) * 5); - date.setSeconds(0); - break; - case 5: - date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break; - default: - date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break; - } - date.setMilliseconds(0); + date.setMinutes(0); + date.setSeconds(0); + date.setMilliseconds(0); + } + else if (this.scale == TimeStep.SCALE.HOUR) { + switch (this.step) { + case 4: + date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break; + default: + date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break; } - else if (this.scale == TimeStep.SCALE.SECOND) { - //noinspection FallthroughInSwitchStatementJS - switch (this.step) { - case 15: - case 10: - date.setSeconds(Math.round(date.getSeconds() / 5) * 5); - date.setMilliseconds(0); - break; - case 5: - date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break; - default: - date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break; - } + date.setSeconds(0); + date.setMilliseconds(0); + } else if (this.scale == TimeStep.SCALE.MINUTE) { + //noinspection FallthroughInSwitchStatementJS + switch (this.step) { + case 15: + case 10: + date.setMinutes(Math.round(date.getMinutes() / 5) * 5); + date.setSeconds(0); + break; + case 5: + date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break; + default: + date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break; } - else if (this.scale == TimeStep.SCALE.MILLISECOND) { - var step = this.step > 5 ? this.step / 2 : 1; - date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step); + date.setMilliseconds(0); + } + else if (this.scale == TimeStep.SCALE.SECOND) { + //noinspection FallthroughInSwitchStatementJS + switch (this.step) { + case 15: + case 10: + date.setSeconds(Math.round(date.getSeconds() / 5) * 5); + date.setMilliseconds(0); + break; + case 5: + date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break; + default: + date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break; } + } + else if (this.scale == TimeStep.SCALE.MILLISECOND) { + var step = this.step > 5 ? this.step / 2 : 1; + date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step); + } }; /** @@ -375,26 +375,26 @@ TimeStep.prototype.snap = function(date) { * @return {boolean} true if current date is major, else false. */ TimeStep.prototype.isMajor = function() { - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND: - return (this.current.getMilliseconds() == 0); - case TimeStep.SCALE.SECOND: - return (this.current.getSeconds() == 0); - case TimeStep.SCALE.MINUTE: - return (this.current.getHours() == 0) && (this.current.getMinutes() == 0); - // Note: this is no bug. Major label is equal for both minute and hour scale - case TimeStep.SCALE.HOUR: - return (this.current.getHours() == 0); - case TimeStep.SCALE.WEEKDAY: // intentional fall through - case TimeStep.SCALE.DAY: - return (this.current.getDate() == 1); - case TimeStep.SCALE.MONTH: - return (this.current.getMonth() == 0); - case TimeStep.SCALE.YEAR: - return false; - default: - return false; - } + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND: + return (this.current.getMilliseconds() == 0); + case TimeStep.SCALE.SECOND: + return (this.current.getSeconds() == 0); + case TimeStep.SCALE.MINUTE: + return (this.current.getHours() == 0) && (this.current.getMinutes() == 0); + // Note: this is no bug. Major label is equal for both minute and hour scale + case TimeStep.SCALE.HOUR: + return (this.current.getHours() == 0); + case TimeStep.SCALE.WEEKDAY: // intentional fall through + case TimeStep.SCALE.DAY: + return (this.current.getDate() == 1); + case TimeStep.SCALE.MONTH: + return (this.current.getMonth() == 0); + case TimeStep.SCALE.YEAR: + return false; + default: + return false; + } }; @@ -405,21 +405,21 @@ TimeStep.prototype.isMajor = function() { * @param {Date} [date] custom date. if not provided, current date is taken */ TimeStep.prototype.getLabelMinor = function(date) { - if (date == undefined) { - date = this.current; - } - - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS'); - case TimeStep.SCALE.SECOND: return moment(date).format('s'); - case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm'); - case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm'); - case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D'); - case TimeStep.SCALE.DAY: return moment(date).format('D'); - case TimeStep.SCALE.MONTH: return moment(date).format('MMM'); - case TimeStep.SCALE.YEAR: return moment(date).format('YYYY'); - default: return ''; - } + if (date == undefined) { + date = this.current; + } + + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS'); + case TimeStep.SCALE.SECOND: return moment(date).format('s'); + case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm'); + case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm'); + case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D'); + case TimeStep.SCALE.DAY: return moment(date).format('D'); + case TimeStep.SCALE.MONTH: return moment(date).format('MMM'); + case TimeStep.SCALE.YEAR: return moment(date).format('YYYY'); + default: return ''; + } }; @@ -430,20 +430,20 @@ TimeStep.prototype.getLabelMinor = function(date) { * @param {Date} [date] custom date. if not provided, current date is taken */ TimeStep.prototype.getLabelMajor = function(date) { - if (date == undefined) { - date = this.current; - } - - //noinspection FallthroughInSwitchStatementJS - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss'); - case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm'); - case TimeStep.SCALE.MINUTE: - case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM'); - case TimeStep.SCALE.WEEKDAY: - case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY'); - case TimeStep.SCALE.MONTH: return moment(date).format('YYYY'); - case TimeStep.SCALE.YEAR: return ''; - default: return ''; - } + if (date == undefined) { + date = this.current; + } + + //noinspection FallthroughInSwitchStatementJS + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss'); + case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm'); + case TimeStep.SCALE.MINUTE: + case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM'); + case TimeStep.SCALE.WEEKDAY: + case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY'); + case TimeStep.SCALE.MONTH: return moment(date).format('YYYY'); + case TimeStep.SCALE.YEAR: return ''; + default: return ''; + } }; diff --git a/src/timeline/Timeline.js b/src/timeline/Timeline.js index 9122be85..cceebc7b 100644 --- a/src/timeline/Timeline.js +++ b/src/timeline/Timeline.js @@ -6,130 +6,130 @@ * @constructor */ function Timeline (container, items, options) { - var me = this; - var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0); - this.options = { - orientation: 'bottom', - min: null, - max: null, - zoomMin: 10, // milliseconds - zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds - // moveable: true, // TODO: option moveable - // zoomable: true, // TODO: option zoomable - showMinorLabels: true, - showMajorLabels: true, - showCurrentTime: false, - showCustomTime: false, - autoResize: false - }; - - // controller - this.controller = new Controller(); - - // root panel - if (!container) { - throw new Error('No container element provided'); + var me = this; + var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0); + this.options = { + orientation: 'bottom', + min: null, + max: null, + zoomMin: 10, // milliseconds + zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds + // moveable: true, // TODO: option moveable + // zoomable: true, // TODO: option zoomable + showMinorLabels: true, + showMajorLabels: true, + showCurrentTime: false, + showCustomTime: false, + autoResize: false + }; + + // controller + this.controller = new Controller(); + + // root panel + if (!container) { + throw new Error('No container element provided'); + } + var rootOptions = Object.create(this.options); + rootOptions.height = function () { + // TODO: change to height + if (me.options.height) { + // fixed height + return me.options.height; } - var rootOptions = Object.create(this.options); - rootOptions.height = function () { - // TODO: change to height - if (me.options.height) { - // fixed height - return me.options.height; - } - else { - // auto height - return (me.timeaxis.height + me.content.height) + 'px'; - } - }; - this.rootPanel = new RootPanel(container, rootOptions); - this.controller.add(this.rootPanel); - - // item panel - var itemOptions = Object.create(this.options); - itemOptions.left = function () { - return me.labelPanel.width; - }; - itemOptions.width = function () { - return me.rootPanel.width - me.labelPanel.width; - }; - itemOptions.top = null; - itemOptions.height = null; - this.itemPanel = new Panel(this.rootPanel, [], itemOptions); - this.controller.add(this.itemPanel); - - // label panel - var labelOptions = Object.create(this.options); - labelOptions.top = null; - labelOptions.left = null; - labelOptions.height = null; - labelOptions.width = function () { - if (me.content && typeof me.content.getLabelsWidth === 'function') { - return me.content.getLabelsWidth(); - } - else { - return 0; - } - }; - this.labelPanel = new Panel(this.rootPanel, [], labelOptions); - this.controller.add(this.labelPanel); - - // range - var rangeOptions = Object.create(this.options); - this.range = new Range(rangeOptions); - this.range.setRange( - now.clone().add('days', -3).valueOf(), - now.clone().add('days', 4).valueOf() - ); - - // TODO: reckon with options moveable and zoomable - this.range.subscribe(this.rootPanel, 'move', 'horizontal'); - this.range.subscribe(this.rootPanel, 'zoom', 'horizontal'); - this.range.on('rangechange', function () { - var force = true; - me.controller.requestReflow(force); - }); - this.range.on('rangechanged', function () { - var force = true; - me.controller.requestReflow(force); - }); - - // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable - - // time axis - var timeaxisOptions = Object.create(rootOptions); - timeaxisOptions.range = this.range; - timeaxisOptions.left = null; - timeaxisOptions.top = null; - timeaxisOptions.width = '100%'; - timeaxisOptions.height = null; - this.timeaxis = new TimeAxis(this.itemPanel, [], timeaxisOptions); - this.timeaxis.setRange(this.range); - this.controller.add(this.timeaxis); - - // current time bar - this.currenttime = new CurrentTime(this.timeaxis, [], rootOptions); - this.controller.add(this.currenttime); - - // custom time bar - this.customtime = new CustomTime(this.timeaxis, [], rootOptions); - this.controller.add(this.customtime); - - // create groupset - this.setGroups(null); - - this.itemsData = null; // DataSet - this.groupsData = null; // DataSet - - // apply options - if (options) { - this.setOptions(options); + else { + // auto height + return (me.timeaxis.height + me.content.height) + 'px'; } - - // create itemset and groupset - if (items) { - this.setItems(items); + }; + this.rootPanel = new RootPanel(container, rootOptions); + this.controller.add(this.rootPanel); + + // item panel + var itemOptions = Object.create(this.options); + itemOptions.left = function () { + return me.labelPanel.width; + }; + itemOptions.width = function () { + return me.rootPanel.width - me.labelPanel.width; + }; + itemOptions.top = null; + itemOptions.height = null; + this.itemPanel = new Panel(this.rootPanel, [], itemOptions); + this.controller.add(this.itemPanel); + + // label panel + var labelOptions = Object.create(this.options); + labelOptions.top = null; + labelOptions.left = null; + labelOptions.height = null; + labelOptions.width = function () { + if (me.content && typeof me.content.getLabelsWidth === 'function') { + return me.content.getLabelsWidth(); } + else { + return 0; + } + }; + this.labelPanel = new Panel(this.rootPanel, [], labelOptions); + this.controller.add(this.labelPanel); + + // range + var rangeOptions = Object.create(this.options); + this.range = new Range(rangeOptions); + this.range.setRange( + now.clone().add('days', -3).valueOf(), + now.clone().add('days', 4).valueOf() + ); + + // TODO: reckon with options moveable and zoomable + this.range.subscribe(this.rootPanel, 'move', 'horizontal'); + this.range.subscribe(this.rootPanel, 'zoom', 'horizontal'); + this.range.on('rangechange', function () { + var force = true; + me.controller.requestReflow(force); + }); + this.range.on('rangechanged', function () { + var force = true; + me.controller.requestReflow(force); + }); + + // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable + + // time axis + var timeaxisOptions = Object.create(rootOptions); + timeaxisOptions.range = this.range; + timeaxisOptions.left = null; + timeaxisOptions.top = null; + timeaxisOptions.width = '100%'; + timeaxisOptions.height = null; + this.timeaxis = new TimeAxis(this.itemPanel, [], timeaxisOptions); + this.timeaxis.setRange(this.range); + this.controller.add(this.timeaxis); + + // current time bar + this.currenttime = new CurrentTime(this.timeaxis, [], rootOptions); + this.controller.add(this.currenttime); + + // custom time bar + this.customtime = new CustomTime(this.timeaxis, [], rootOptions); + this.controller.add(this.customtime); + + // create groupset + this.setGroups(null); + + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet + + // apply options + if (options) { + this.setOptions(options); + } + + // create itemset and groupset + if (items) { + this.setItems(items); + } } /** @@ -137,15 +137,15 @@ function Timeline (container, items, options) { * @param {Object} options TODO: describe the available options */ Timeline.prototype.setOptions = function (options) { - util.extend(this.options, options); + util.extend(this.options, options); - // force update of range - // options.start and options.end can be undefined - //this.range.setRange(options.start, options.end); - this.range.setRange(); + // force update of range + // options.start and options.end can be undefined + //this.range.setRange(options.start, options.end); + this.range.setRange(); - this.controller.reflow(); - this.controller.repaint(); + this.controller.reflow(); + this.controller.repaint(); }; /** @@ -153,7 +153,7 @@ Timeline.prototype.setOptions = function (options) { * @param {Date} time */ Timeline.prototype.setCustomTime = function (time) { - this.customtime._setCustomTime(time); + this.customtime._setCustomTime(time); }; /** @@ -161,7 +161,7 @@ Timeline.prototype.setCustomTime = function (time) { * @return {Date} customTime */ Timeline.prototype.getCustomTime = function() { - return new Date(this.customtime.customTime.valueOf()); + return new Date(this.customtime.customTime.valueOf()); }; /** @@ -169,60 +169,60 @@ Timeline.prototype.getCustomTime = function() { * @param {vis.DataSet | Array | DataTable | null} items */ Timeline.prototype.setItems = function(items) { - var initialLoad = (this.itemsData == null); - - // convert to type DataSet when needed - var newItemSet; - if (!items) { - newItemSet = null; + var initialLoad = (this.itemsData == null); + + // convert to type DataSet when needed + var newItemSet; + if (!items) { + newItemSet = null; + } + else if (items instanceof DataSet) { + newItemSet = items; + } + if (!(items instanceof DataSet)) { + newItemSet = new DataSet({ + convert: { + start: 'Date', + end: 'Date' + } + }); + newItemSet.add(items); + } + + // set items + this.itemsData = newItemSet; + this.content.setItems(newItemSet); + + if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) { + // apply the data range as range + var dataRange = this.getItemRange(); + + // add 5% space on both sides + var min = dataRange.min; + var max = dataRange.max; + if (min != null && max != null) { + var interval = (max.valueOf() - min.valueOf()); + if (interval <= 0) { + // prevent an empty interval + interval = 24 * 60 * 60 * 1000; // 1 day + } + min = new Date(min.valueOf() - interval * 0.05); + max = new Date(max.valueOf() + interval * 0.05); } - else if (items instanceof DataSet) { - newItemSet = items; + + // override specified start and/or end date + if (this.options.start != undefined) { + min = util.convert(this.options.start, 'Date'); } - if (!(items instanceof DataSet)) { - newItemSet = new DataSet({ - convert: { - start: 'Date', - end: 'Date' - } - }); - newItemSet.add(items); + if (this.options.end != undefined) { + max = util.convert(this.options.end, 'Date'); } - // set items - this.itemsData = newItemSet; - this.content.setItems(newItemSet); - - if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) { - // apply the data range as range - var dataRange = this.getItemRange(); - - // add 5% space on both sides - var min = dataRange.min; - var max = dataRange.max; - if (min != null && max != null) { - var interval = (max.valueOf() - min.valueOf()); - if (interval <= 0) { - // prevent an empty interval - interval = 24 * 60 * 60 * 1000; // 1 day - } - min = new Date(min.valueOf() - interval * 0.05); - max = new Date(max.valueOf() + interval * 0.05); - } - - // override specified start and/or end date - if (this.options.start != undefined) { - min = util.convert(this.options.start, 'Date'); - } - if (this.options.end != undefined) { - max = util.convert(this.options.end, 'Date'); - } - - // apply range if there is a min or max available - if (min != null || max != null) { - this.range.setRange(min, max); - } + // apply range if there is a min or max available + if (min != null || max != null) { + this.range.setRange(min, max); } + } }; /** @@ -230,76 +230,76 @@ Timeline.prototype.setItems = function(items) { * @param {vis.DataSet | Array | DataTable} groups */ Timeline.prototype.setGroups = function(groups) { - var me = this; - this.groupsData = groups; - - // switch content type between ItemSet or GroupSet when needed - var Type = this.groupsData ? GroupSet : ItemSet; - if (!(this.content instanceof Type)) { - // remove old content set - if (this.content) { - this.content.hide(); - if (this.content.setItems) { - this.content.setItems(); // disconnect from items - } - if (this.content.setGroups) { - this.content.setGroups(); // disconnect from groups - } - this.controller.remove(this.content); - } + var me = this; + this.groupsData = groups; + + // switch content type between ItemSet or GroupSet when needed + var Type = this.groupsData ? GroupSet : ItemSet; + if (!(this.content instanceof Type)) { + // remove old content set + if (this.content) { + this.content.hide(); + if (this.content.setItems) { + this.content.setItems(); // disconnect from items + } + if (this.content.setGroups) { + this.content.setGroups(); // disconnect from groups + } + this.controller.remove(this.content); + } - // create new content set - var options = Object.create(this.options); - util.extend(options, { - top: function () { - if (me.options.orientation == 'top') { - return me.timeaxis.height; - } - else { - return me.itemPanel.height - me.timeaxis.height - me.content.height; - } - }, - left: null, - width: '100%', - height: function () { - if (me.options.height) { - // fixed height - return me.itemPanel.height - me.timeaxis.height; - } - else { - // auto height - return null; - } - }, - maxHeight: function () { - // TODO: change maxHeight to be a css string like '100%' or '300px' - if (me.options.maxHeight) { - if (!util.isNumber(me.options.maxHeight)) { - throw new TypeError('Number expected for property maxHeight'); - } - return me.options.maxHeight - me.timeaxis.height; - } - else { - return null; - } - }, - labelContainer: function () { - return me.labelPanel.getContainer(); - } - }); - - this.content = new Type(this.itemPanel, [this.timeaxis], options); - if (this.content.setRange) { - this.content.setRange(this.range); + // create new content set + var options = Object.create(this.options); + util.extend(options, { + top: function () { + if (me.options.orientation == 'top') { + return me.timeaxis.height; } - if (this.content.setItems) { - this.content.setItems(this.itemsData); + else { + return me.itemPanel.height - me.timeaxis.height - me.content.height; } - if (this.content.setGroups) { - this.content.setGroups(this.groupsData); + }, + left: null, + width: '100%', + height: function () { + if (me.options.height) { + // fixed height + return me.itemPanel.height - me.timeaxis.height; + } + else { + // auto height + return null; } - this.controller.add(this.content); + }, + maxHeight: function () { + // TODO: change maxHeight to be a css string like '100%' or '300px' + if (me.options.maxHeight) { + if (!util.isNumber(me.options.maxHeight)) { + throw new TypeError('Number expected for property maxHeight'); + } + return me.options.maxHeight - me.timeaxis.height; + } + else { + return null; + } + }, + labelContainer: function () { + return me.labelPanel.getContainer(); + } + }); + + this.content = new Type(this.itemPanel, [this.timeaxis], options); + if (this.content.setRange) { + this.content.setRange(this.range); } + if (this.content.setItems) { + this.content.setItems(this.itemsData); + } + if (this.content.setGroups) { + this.content.setGroups(this.groupsData); + } + this.controller.add(this.content); + } }; /** @@ -309,34 +309,34 @@ Timeline.prototype.setGroups = function(groups) { * When no maximum is found, max==null */ Timeline.prototype.getItemRange = function getItemRange() { - // calculate min from start filed - var itemsData = this.itemsData, - min = null, - max = null; - - if (itemsData) { - // calculate the minimum value of the field 'start' - var minItem = itemsData.min('start'); - min = minItem ? minItem.start.valueOf() : null; - - // calculate maximum value of fields 'start' and 'end' - var maxStartItem = itemsData.max('start'); - if (maxStartItem) { - max = maxStartItem.start.valueOf(); - } - var maxEndItem = itemsData.max('end'); - if (maxEndItem) { - if (max == null) { - max = maxEndItem.end.valueOf(); - } - else { - max = Math.max(max, maxEndItem.end.valueOf()); - } - } + // calculate min from start filed + var itemsData = this.itemsData, + min = null, + max = null; + + if (itemsData) { + // calculate the minimum value of the field 'start' + var minItem = itemsData.min('start'); + min = minItem ? minItem.start.valueOf() : null; + + // calculate maximum value of fields 'start' and 'end' + var maxStartItem = itemsData.max('start'); + if (maxStartItem) { + max = maxStartItem.start.valueOf(); + } + var maxEndItem = itemsData.max('end'); + if (maxEndItem) { + if (max == null) { + max = maxEndItem.end.valueOf(); + } + else { + max = Math.max(max, maxEndItem.end.valueOf()); + } } + } - return { - min: (min != null) ? new Date(min) : null, - max: (max != null) ? new Date(max) : null - }; + return { + min: (min != null) ? new Date(min) : null, + max: (max != null) ? new Date(max) : null + }; }; diff --git a/src/timeline/component/Component.js b/src/timeline/component/Component.js index 7c21e369..8d15e0c6 100644 --- a/src/timeline/component/Component.js +++ b/src/timeline/component/Component.js @@ -2,17 +2,17 @@ * Prototype for visual components */ function Component () { - this.id = null; - this.parent = null; - this.depends = null; - this.controller = null; - this.options = null; + this.id = null; + this.parent = null; + this.depends = null; + this.controller = null; + this.options = null; - this.frame = null; // main DOM element - this.top = 0; - this.left = 0; - this.width = 0; - this.height = 0; + this.frame = null; // main DOM element + this.top = 0; + this.left = 0; + this.width = 0; + this.height = 0; } /** @@ -27,14 +27,14 @@ function Component () { * {String | Number | function} [height] */ Component.prototype.setOptions = function setOptions(options) { - if (options) { - util.extend(this.options, options); + if (options) { + util.extend(this.options, options); - if (this.controller) { - this.requestRepaint(); - this.requestReflow(); - } + if (this.controller) { + this.requestRepaint(); + this.requestReflow(); } + } }; /** @@ -45,14 +45,14 @@ Component.prototype.setOptions = function setOptions(options) { * @return {*} value */ Component.prototype.getOption = function getOption(name) { - var value; - if (this.options) { - value = this.options[name]; - } - if (value === undefined && this.defaultOptions) { - value = this.defaultOptions[name]; - } - return value; + var value; + if (this.options) { + value = this.options[name]; + } + if (value === undefined && this.defaultOptions) { + value = this.defaultOptions[name]; + } + return value; }; /** @@ -63,8 +63,8 @@ Component.prototype.getOption = function getOption(name) { */ // TODO: get rid of the getContainer and getFrame methods, provide these via the options Component.prototype.getContainer = function getContainer() { - // should be implemented by the component - return null; + // should be implemented by the component + return null; }; /** @@ -72,7 +72,7 @@ Component.prototype.getContainer = function getContainer() { * @returns {HTMLElement | null} frame */ Component.prototype.getFrame = function getFrame() { - return this.frame; + return this.frame; }; /** @@ -80,8 +80,8 @@ Component.prototype.getFrame = function getFrame() { * @return {Boolean} changed */ Component.prototype.repaint = function repaint() { - // should be implemented by the component - return false; + // should be implemented by the component + return false; }; /** @@ -89,8 +89,8 @@ Component.prototype.repaint = function repaint() { * @return {Boolean} resized */ Component.prototype.reflow = function reflow() { - // should be implemented by the component - return false; + // should be implemented by the component + return false; }; /** @@ -98,13 +98,13 @@ Component.prototype.reflow = function reflow() { * @return {Boolean} changed */ Component.prototype.hide = function hide() { - if (this.frame && this.frame.parentNode) { - this.frame.parentNode.removeChild(this.frame); - return true; - } - else { - return false; - } + if (this.frame && this.frame.parentNode) { + this.frame.parentNode.removeChild(this.frame); + return true; + } + else { + return false; + } }; /** @@ -113,36 +113,36 @@ Component.prototype.hide = function hide() { * @return {Boolean} changed */ Component.prototype.show = function show() { - if (!this.frame || !this.frame.parentNode) { - return this.repaint(); - } - else { - return false; - } + if (!this.frame || !this.frame.parentNode) { + return this.repaint(); + } + else { + return false; + } }; /** * Request a repaint. The controller will schedule a repaint */ Component.prototype.requestRepaint = function requestRepaint() { - if (this.controller) { - this.controller.requestRepaint(); - } - else { - throw new Error('Cannot request a repaint: no controller configured'); - // TODO: just do a repaint when no parent is configured? - } + if (this.controller) { + this.controller.requestRepaint(); + } + else { + throw new Error('Cannot request a repaint: no controller configured'); + // TODO: just do a repaint when no parent is configured? + } }; /** * Request a reflow. The controller will schedule a reflow */ Component.prototype.requestReflow = function requestReflow() { - if (this.controller) { - this.controller.requestReflow(); - } - else { - throw new Error('Cannot request a reflow: no controller configured'); - // TODO: just do a reflow when no parent is configured? - } + if (this.controller) { + this.controller.requestReflow(); + } + else { + throw new Error('Cannot request a reflow: no controller configured'); + // TODO: just do a reflow when no parent is configured? + } }; diff --git a/src/timeline/component/ContentPanel.js b/src/timeline/component/ContentPanel.js index ff61a6e2..fcc27167 100644 --- a/src/timeline/component/ContentPanel.js +++ b/src/timeline/component/ContentPanel.js @@ -14,11 +14,11 @@ * @extends Panel */ function ContentPanel(parent, depends, options) { - this.id = util.randomUUID(); - this.parent = parent; - this.depends = depends; + this.id = util.randomUUID(); + this.parent = parent; + this.depends = depends; - this.options = options || {}; + this.options = options || {}; } ContentPanel.prototype = new Component(); @@ -40,7 +40,7 @@ ContentPanel.prototype.setOptions = Component.prototype.setOptions; * @returns {HTMLElement} container */ ContentPanel.prototype.getContainer = function () { - return this.frame; + return this.frame; }; /** @@ -48,46 +48,46 @@ ContentPanel.prototype.getContainer = function () { * @return {Boolean} changed */ ContentPanel.prototype.repaint = function () { - var changed = 0, - update = util.updateProperty, - asSize = util.option.asSize, - options = this.options, - frame = this.frame; - if (!frame) { - frame = document.createElement('div'); - frame.className = 'content-panel'; + var changed = 0, + update = util.updateProperty, + asSize = util.option.asSize, + options = this.options, + frame = this.frame; + if (!frame) { + frame = document.createElement('div'); + frame.className = 'content-panel'; - var className = options.className; - if (className) { - if (typeof className == 'function') { - util.addClassName(frame, String(className())); - } - else { - util.addClassName(frame, String(className)); - } - } + var className = options.className; + if (className) { + if (typeof className == 'function') { + util.addClassName(frame, String(className())); + } + else { + util.addClassName(frame, String(className)); + } + } - this.frame = frame; - changed += 1; + this.frame = frame; + changed += 1; + } + if (!frame.parentNode) { + if (!this.parent) { + throw new Error('Cannot repaint panel: no parent attached'); } - if (!frame.parentNode) { - if (!this.parent) { - throw new Error('Cannot repaint panel: no parent attached'); - } - var parentContainer = this.parent.getContainer(); - if (!parentContainer) { - throw new Error('Cannot repaint panel: parent has no container element'); - } - parentContainer.appendChild(frame); - changed += 1; + var parentContainer = this.parent.getContainer(); + if (!parentContainer) { + throw new Error('Cannot repaint panel: parent has no container element'); } + parentContainer.appendChild(frame); + changed += 1; + } - changed += update(frame.style, 'top', asSize(options.top, '0px')); - changed += update(frame.style, 'left', asSize(options.left, '0px')); - changed += update(frame.style, 'width', asSize(options.width, '100%')); - changed += update(frame.style, 'height', asSize(options.height, '100%')); + changed += update(frame.style, 'top', asSize(options.top, '0px')); + changed += update(frame.style, 'left', asSize(options.left, '0px')); + changed += update(frame.style, 'width', asSize(options.width, '100%')); + changed += update(frame.style, 'height', asSize(options.height, '100%')); - return (changed > 0); + return (changed > 0); }; /** @@ -95,19 +95,19 @@ ContentPanel.prototype.repaint = function () { * @return {Boolean} resized */ ContentPanel.prototype.reflow = function () { - var changed = 0, - update = util.updateProperty, - frame = this.frame; + var changed = 0, + update = util.updateProperty, + frame = this.frame; - if (frame) { - changed += update(this, 'top', frame.offsetTop); - changed += update(this, 'left', frame.offsetLeft); - changed += update(this, 'width', frame.offsetWidth); - changed += update(this, 'height', frame.offsetHeight); - } - else { - changed += 1; - } + if (frame) { + changed += update(this, 'top', frame.offsetTop); + changed += update(this, 'left', frame.offsetLeft); + changed += update(this, 'width', frame.offsetWidth); + changed += update(this, 'height', frame.offsetHeight); + } + else { + changed += 1; + } - return (changed > 0); + return (changed > 0); }; diff --git a/src/timeline/component/CurrentTime.js b/src/timeline/component/CurrentTime.js index a94fd005..ddacf868 100644 --- a/src/timeline/component/CurrentTime.js +++ b/src/timeline/component/CurrentTime.js @@ -10,14 +10,14 @@ */ function CurrentTime (parent, depends, options) { - this.id = util.randomUUID(); - this.parent = parent; - this.depends = depends; - - this.options = options || {}; - this.defaultOptions = { - showCurrentTime: false - }; + this.id = util.randomUUID(); + this.parent = parent; + this.depends = depends; + + this.options = options || {}; + this.defaultOptions = { + showCurrentTime: false + }; } CurrentTime.prototype = new Component(); @@ -30,7 +30,7 @@ CurrentTime.prototype.setOptions = Component.prototype.setOptions; * @returns {HTMLElement} container */ CurrentTime.prototype.getContainer = function () { - return this.frame; + return this.frame; }; /** @@ -38,64 +38,64 @@ CurrentTime.prototype.getContainer = function () { * @return {Boolean} changed */ CurrentTime.prototype.repaint = function () { - var bar = this.frame, - parent = this.parent, - parentContainer = parent.parent.getContainer(); - - if (!parent) { - throw new Error('Cannot repaint bar: no parent attached'); - } - - if (!parentContainer) { - throw new Error('Cannot repaint bar: parent has no container element'); + var bar = this.frame, + parent = this.parent, + parentContainer = parent.parent.getContainer(); + + if (!parent) { + throw new Error('Cannot repaint bar: no parent attached'); + } + + if (!parentContainer) { + throw new Error('Cannot repaint bar: parent has no container element'); + } + + if (!this.getOption('showCurrentTime')) { + if (bar) { + parentContainer.removeChild(bar); + delete this.frame; } - if (!this.getOption('showCurrentTime')) { - if (bar) { - parentContainer.removeChild(bar); - delete this.frame; - } + return; + } - return; - } + if (!bar) { + bar = document.createElement('div'); + bar.className = 'currenttime'; + bar.style.position = 'absolute'; + bar.style.top = '0px'; + bar.style.height = '100%'; - if (!bar) { - bar = document.createElement('div'); - bar.className = 'currenttime'; - bar.style.position = 'absolute'; - bar.style.top = '0px'; - bar.style.height = '100%'; + parentContainer.appendChild(bar); + this.frame = bar; + } - parentContainer.appendChild(bar); - this.frame = bar; - } + if (!parent.conversion) { + parent._updateConversion(); + } - if (!parent.conversion) { - parent._updateConversion(); - } + var now = new Date(); + var x = parent.toScreen(now); - var now = new Date(); - var x = parent.toScreen(now); + bar.style.left = x + 'px'; + bar.title = 'Current time: ' + now; - bar.style.left = x + 'px'; - bar.title = 'Current time: ' + now; + // start a timer to adjust for the new time + if (this.currentTimeTimer !== undefined) { + clearTimeout(this.currentTimeTimer); + delete this.currentTimeTimer; + } - // start a timer to adjust for the new time - if (this.currentTimeTimer !== undefined) { - clearTimeout(this.currentTimeTimer); - delete this.currentTimeTimer; - } + var timeline = this; + var interval = 1 / parent.conversion.scale / 2; - var timeline = this; - var interval = 1 / parent.conversion.scale / 2; + if (interval < 30) { + interval = 30; + } - if (interval < 30) { - interval = 30; - } - - this.currentTimeTimer = setTimeout(function() { - timeline.repaint(); - }, interval); + this.currentTimeTimer = setTimeout(function() { + timeline.repaint(); + }, interval); - return false; + return false; }; diff --git a/src/timeline/component/CustomTime.js b/src/timeline/component/CustomTime.js index 9efe6892..a5df5ca5 100644 --- a/src/timeline/component/CustomTime.js +++ b/src/timeline/component/CustomTime.js @@ -10,17 +10,17 @@ */ function CustomTime (parent, depends, options) { - this.id = util.randomUUID(); - this.parent = parent; - this.depends = depends; + this.id = util.randomUUID(); + this.parent = parent; + this.depends = depends; - this.options = options || {}; - this.defaultOptions = { - showCustomTime: false - }; + this.options = options || {}; + this.defaultOptions = { + showCustomTime: false + }; - this.listeners = []; - this.customTime = new Date(); + this.listeners = []; + this.customTime = new Date(); } CustomTime.prototype = new Component(); @@ -33,7 +33,7 @@ CustomTime.prototype.setOptions = Component.prototype.setOptions; * @returns {HTMLElement} container */ CustomTime.prototype.getContainer = function () { - return this.frame; + return this.frame; }; /** @@ -41,59 +41,59 @@ CustomTime.prototype.getContainer = function () { * @return {Boolean} changed */ CustomTime.prototype.repaint = function () { - var bar = this.frame, - parent = this.parent, - parentContainer = parent.parent.getContainer(); - - if (!parent) { - throw new Error('Cannot repaint bar: no parent attached'); + var bar = this.frame, + parent = this.parent, + parentContainer = parent.parent.getContainer(); + + if (!parent) { + throw new Error('Cannot repaint bar: no parent attached'); + } + + if (!parentContainer) { + throw new Error('Cannot repaint bar: parent has no container element'); + } + + if (!this.getOption('showCustomTime')) { + if (bar) { + parentContainer.removeChild(bar); + delete this.frame; } - if (!parentContainer) { - throw new Error('Cannot repaint bar: parent has no container element'); - } + return; + } - if (!this.getOption('showCustomTime')) { - if (bar) { - parentContainer.removeChild(bar); - delete this.frame; - } + if (!bar) { + bar = document.createElement('div'); + bar.className = 'customtime'; + bar.style.position = 'absolute'; + bar.style.top = '0px'; + bar.style.height = '100%'; - return; - } + parentContainer.appendChild(bar); - if (!bar) { - bar = document.createElement('div'); - bar.className = 'customtime'; - bar.style.position = 'absolute'; - bar.style.top = '0px'; - bar.style.height = '100%'; + var drag = document.createElement('div'); + drag.style.position = 'relative'; + drag.style.top = '0px'; + drag.style.left = '-10px'; + drag.style.height = '100%'; + drag.style.width = '20px'; + bar.appendChild(drag); - parentContainer.appendChild(bar); + this.frame = bar; - var drag = document.createElement('div'); - drag.style.position = 'relative'; - drag.style.top = '0px'; - drag.style.left = '-10px'; - drag.style.height = '100%'; - drag.style.width = '20px'; - bar.appendChild(drag); + this.subscribe(this, 'movetime'); + } - this.frame = bar; + if (!parent.conversion) { + parent._updateConversion(); + } - this.subscribe(this, 'movetime'); - } - - if (!parent.conversion) { - parent._updateConversion(); - } + var x = parent.toScreen(this.customTime); - var x = parent.toScreen(this.customTime); + bar.style.left = x + 'px'; + bar.title = 'Time: ' + this.customTime; - bar.style.left = x + 'px'; - bar.title = 'Time: ' + this.customTime; - - return false; + return false; }; /** @@ -101,8 +101,8 @@ CustomTime.prototype.repaint = function () { * @param {Date} time */ CustomTime.prototype._setCustomTime = function(time) { - this.customTime = new Date(time.valueOf()); - this.repaint(); + this.customTime = new Date(time.valueOf()); + this.repaint(); }; /** @@ -110,7 +110,7 @@ CustomTime.prototype._setCustomTime = function(time) { * @return {Date} customTime */ CustomTime.prototype._getCustomTime = function() { - return new Date(this.customTime.valueOf()); + return new Date(this.customTime.valueOf()); }; /** @@ -118,18 +118,18 @@ CustomTime.prototype._getCustomTime = function() { * @param {Component} component */ CustomTime.prototype.subscribe = function (component, event) { - var me = this; - var listener = { - component: component, - event: event, - callback: function (event) { - me._onMouseDown(event, listener); - }, - params: {} - }; - - component.on('mousedown', listener.callback); - me.listeners.push(listener); + var me = this; + var listener = { + component: component, + event: event, + callback: function (event) { + me._onMouseDown(event, listener); + }, + params: {} + }; + + component.on('mousedown', listener.callback); + me.listeners.push(listener); }; @@ -140,13 +140,13 @@ CustomTime.prototype.subscribe = function (component, event) { * as parameter. */ CustomTime.prototype.on = function (event, callback) { - var bar = this.frame; - if (!bar) { - throw new Error('Cannot add event listener: no parent attached'); - } + var bar = this.frame; + if (!bar) { + throw new Error('Cannot add event listener: no parent attached'); + } - events.addListener(this, event, callback); - util.addEventListener(bar, event, callback); + events.addListener(this, event, callback); + util.addEventListener(bar, event, callback); }; /** @@ -156,38 +156,38 @@ CustomTime.prototype.on = function (event, callback) { * @private */ CustomTime.prototype._onMouseDown = function(event, listener) { - event = event || window.event; - var params = listener.params; - - // only react on left mouse button down - var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1); - if (!leftButtonDown) { - return; - } - - // get mouse position - params.mouseX = util.getPageX(event); - params.moved = false; - - params.customTime = this.customTime; - - // add event listeners to handle moving the custom time bar - var me = this; - if (!params.onMouseMove) { - params.onMouseMove = function (event) { - me._onMouseMove(event, listener); - }; - util.addEventListener(document, 'mousemove', params.onMouseMove); - } - if (!params.onMouseUp) { - params.onMouseUp = function (event) { - me._onMouseUp(event, listener); - }; - util.addEventListener(document, 'mouseup', params.onMouseUp); - } + event = event || window.event; + var params = listener.params; + + // only react on left mouse button down + var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1); + if (!leftButtonDown) { + return; + } + + // get mouse position + params.mouseX = util.getPageX(event); + params.moved = false; + + params.customTime = this.customTime; + + // add event listeners to handle moving the custom time bar + var me = this; + if (!params.onMouseMove) { + params.onMouseMove = function (event) { + me._onMouseMove(event, listener); + }; + util.addEventListener(document, 'mousemove', params.onMouseMove); + } + if (!params.onMouseUp) { + params.onMouseUp = function (event) { + me._onMouseUp(event, listener); + }; + util.addEventListener(document, 'mouseup', params.onMouseUp); + } - util.stopPropagation(event); - util.preventDefault(event); + util.stopPropagation(event); + util.preventDefault(event); }; /** @@ -198,33 +198,33 @@ CustomTime.prototype._onMouseDown = function(event, listener) { * @private */ CustomTime.prototype._onMouseMove = function (event, listener) { - event = event || window.event; - var params = listener.params; - var parent = this.parent; + event = event || window.event; + var params = listener.params; + var parent = this.parent; - // calculate change in mouse position - var mouseX = util.getPageX(event); + // calculate change in mouse position + var mouseX = util.getPageX(event); - if (params.mouseX === undefined) { - params.mouseX = mouseX; - } + if (params.mouseX === undefined) { + params.mouseX = mouseX; + } - var diff = mouseX - params.mouseX; + var diff = mouseX - params.mouseX; - // if mouse movement is big enough, register it as a "moved" event - if (Math.abs(diff) >= 1) { - params.moved = true; - } + // if mouse movement is big enough, register it as a "moved" event + if (Math.abs(diff) >= 1) { + params.moved = true; + } - var x = parent.toScreen(params.customTime); - var xnew = x + diff; - var time = parent.toTime(xnew); - this._setCustomTime(time); + var x = parent.toScreen(params.customTime); + var xnew = x + diff; + var time = parent.toTime(xnew); + this._setCustomTime(time); - // fire a timechange event - events.trigger(this, 'timechange', {customTime: this.customTime}); + // fire a timechange event + events.trigger(this, 'timechange', {customTime: this.customTime}); - util.preventDefault(event); + util.preventDefault(event); }; /** @@ -235,21 +235,21 @@ CustomTime.prototype._onMouseMove = function (event, listener) { * @private */ CustomTime.prototype._onMouseUp = function (event, listener) { - event = event || window.event; - var params = listener.params; - - // remove event listeners here, important for Safari - if (params.onMouseMove) { - util.removeEventListener(document, 'mousemove', params.onMouseMove); - params.onMouseMove = null; - } - if (params.onMouseUp) { - util.removeEventListener(document, 'mouseup', params.onMouseUp); - params.onMouseUp = null; - } - - if (params.moved) { - // fire a timechanged event - events.trigger(this, 'timechanged', {customTime: this.customTime}); - } + event = event || window.event; + var params = listener.params; + + // remove event listeners here, important for Safari + if (params.onMouseMove) { + util.removeEventListener(document, 'mousemove', params.onMouseMove); + params.onMouseMove = null; + } + if (params.onMouseUp) { + util.removeEventListener(document, 'mouseup', params.onMouseUp); + params.onMouseUp = null; + } + + if (params.moved) { + // fire a timechanged event + events.trigger(this, 'timechanged', {customTime: this.customTime}); + } }; diff --git a/src/timeline/component/Group.js b/src/timeline/component/Group.js index 1198cd3c..74fac5dc 100644 --- a/src/timeline/component/Group.js +++ b/src/timeline/component/Group.js @@ -7,25 +7,25 @@ * @extends Component */ function Group (parent, groupId, options) { - this.id = util.randomUUID(); - this.parent = parent; - - this.groupId = groupId; - this.itemset = null; // ItemSet - this.options = options || {}; - this.options.top = 0; - - this.props = { - label: { - width: 0, - height: 0 - } - }; - - this.top = 0; - this.left = 0; - this.width = 0; - this.height = 0; + this.id = util.randomUUID(); + this.parent = parent; + + this.groupId = groupId; + this.itemset = null; // ItemSet + this.options = options || {}; + this.options.top = 0; + + this.props = { + label: { + width: 0, + height: 0 + } + }; + + this.top = 0; + this.left = 0; + this.width = 0; + this.height = 0; } Group.prototype = new Component(); @@ -39,7 +39,7 @@ Group.prototype.setOptions = Component.prototype.setOptions; * @returns {HTMLElement} container */ Group.prototype.getContainer = function () { - return this.parent.getContainer(); + return this.parent.getContainer(); }; /** @@ -48,31 +48,31 @@ Group.prototype.getContainer = function () { * @param {DataSet | DataView} items */ Group.prototype.setItems = function setItems(items) { - if (this.itemset) { - // remove current item set - this.itemset.hide(); - this.itemset.setItems(); - - this.parent.controller.remove(this.itemset); - this.itemset = null; - } - - if (items) { - var groupId = this.groupId; - - var itemsetOptions = Object.create(this.options); - this.itemset = new ItemSet(this, null, itemsetOptions); - this.itemset.setRange(this.parent.range); - - this.view = new DataView(items, { - filter: function (item) { - return item.group == groupId; - } - }); - this.itemset.setItems(this.view); - - this.parent.controller.add(this.itemset); - } + if (this.itemset) { + // remove current item set + this.itemset.hide(); + this.itemset.setItems(); + + this.parent.controller.remove(this.itemset); + this.itemset = null; + } + + if (items) { + var groupId = this.groupId; + + var itemsetOptions = Object.create(this.options); + this.itemset = new ItemSet(this, null, itemsetOptions); + this.itemset.setRange(this.parent.range); + + this.view = new DataView(items, { + filter: function (item) { + return item.group == groupId; + } + }); + this.itemset.setItems(this.view); + + this.parent.controller.add(this.itemset); + } }; /** @@ -80,7 +80,7 @@ Group.prototype.setItems = function setItems(items) { * @return {Boolean} changed */ Group.prototype.repaint = function repaint() { - return false; + return false; }; /** @@ -88,23 +88,23 @@ Group.prototype.repaint = function repaint() { * @return {Boolean} resized */ Group.prototype.reflow = function reflow() { - var changed = 0, - update = util.updateProperty; + var changed = 0, + update = util.updateProperty; - changed += update(this, 'top', this.itemset ? this.itemset.top : 0); - changed += update(this, 'height', this.itemset ? this.itemset.height : 0); + changed += update(this, 'top', this.itemset ? this.itemset.top : 0); + changed += update(this, 'height', this.itemset ? this.itemset.height : 0); - // TODO: reckon with the height of the group label + // TODO: reckon with the height of the group label - if (this.label) { - var inner = this.label.firstChild; - changed += update(this.props.label, 'width', inner.clientWidth); - changed += update(this.props.label, 'height', inner.clientHeight); - } - else { - changed += update(this.props.label, 'width', 0); - changed += update(this.props.label, 'height', 0); - } + if (this.label) { + var inner = this.label.firstChild; + changed += update(this.props.label, 'width', inner.clientWidth); + changed += update(this.props.label, 'height', inner.clientHeight); + } + else { + changed += update(this.props.label, 'width', 0); + changed += update(this.props.label, 'height', 0); + } - return (changed > 0); + return (changed > 0); }; diff --git a/src/timeline/component/GroupSet.js b/src/timeline/component/GroupSet.js index 616bb42c..970a83d6 100644 --- a/src/timeline/component/GroupSet.js +++ b/src/timeline/component/GroupSet.js @@ -9,42 +9,42 @@ * @extends Panel */ function GroupSet(parent, depends, options) { - this.id = util.randomUUID(); - this.parent = parent; - this.depends = depends; + this.id = util.randomUUID(); + this.parent = parent; + this.depends = depends; - this.options = options || {}; + this.options = options || {}; - this.range = null; // Range or Object {start: number, end: number} - this.itemsData = null; // DataSet with items - this.groupsData = null; // DataSet with groups + this.range = null; // Range or Object {start: number, end: number} + this.itemsData = null; // DataSet with items + this.groupsData = null; // DataSet with groups - this.groups = {}; // map with groups + this.groups = {}; // map with groups - this.dom = {}; - this.props = { - labels: { - width: 0 - } - }; - - // TODO: implement right orientation of the labels - - // changes in groups are queued key/value map containing id/action - this.queue = {}; - - var me = this; - this.listeners = { - 'add': function (event, params) { - me._onAdd(params.items); - }, - 'update': function (event, params) { - me._onUpdate(params.items); - }, - 'remove': function (event, params) { - me._onRemove(params.items); - } - }; + this.dom = {}; + this.props = { + labels: { + width: 0 + } + }; + + // TODO: implement right orientation of the labels + + // changes in groups are queued key/value map containing id/action + this.queue = {}; + + var me = this; + this.listeners = { + 'add': function (event, params) { + me._onAdd(params.items); + }, + 'update': function (event, params) { + me._onUpdate(params.items); + }, + 'remove': function (event, params) { + me._onRemove(params.items); + } + }; } GroupSet.prototype = new Panel(); @@ -58,7 +58,7 @@ GroupSet.prototype = new Panel(); GroupSet.prototype.setOptions = Component.prototype.setOptions; GroupSet.prototype.setRange = function (range) { - // TODO: implement setRange + // TODO: implement setRange }; /** @@ -66,14 +66,14 @@ GroupSet.prototype.setRange = function (range) { * @param {vis.DataSet | null} items */ GroupSet.prototype.setItems = function setItems(items) { - this.itemsData = items; + this.itemsData = items; - for (var id in this.groups) { - if (this.groups.hasOwnProperty(id)) { - var group = this.groups[id]; - group.setItems(items); - } + for (var id in this.groups) { + if (this.groups.hasOwnProperty(id)) { + var group = this.groups[id]; + group.setItems(items); } + } }; /** @@ -81,7 +81,7 @@ GroupSet.prototype.setItems = function setItems(items) { * @return {vis.DataSet | null} items */ GroupSet.prototype.getItems = function getItems() { - return this.itemsData; + return this.itemsData; }; /** @@ -89,7 +89,7 @@ GroupSet.prototype.getItems = function getItems() { * @param {Range | Object} range A Range or an object containing start and end. */ GroupSet.prototype.setRange = function setRange(range) { - this.range = range; + this.range = range; }; /** @@ -97,48 +97,48 @@ GroupSet.prototype.setRange = function setRange(range) { * @param {vis.DataSet} groups */ GroupSet.prototype.setGroups = function setGroups(groups) { - var me = this, - ids; - - // unsubscribe from current dataset - if (this.groupsData) { - util.forEach(this.listeners, function (callback, event) { - me.groupsData.unsubscribe(event, callback); - }); + var me = this, + ids; - // remove all drawn groups - ids = this.groupsData.getIds(); - this._onRemove(ids); - } - - // replace the dataset - if (!groups) { - this.groupsData = null; - } - else if (groups instanceof DataSet) { - this.groupsData = groups; - } - else { - this.groupsData = new DataSet({ - convert: { - start: 'Date', - end: 'Date' - } - }); - this.groupsData.add(groups); - } + // unsubscribe from current dataset + if (this.groupsData) { + util.forEach(this.listeners, function (callback, event) { + me.groupsData.unsubscribe(event, callback); + }); - if (this.groupsData) { - // subscribe to new dataset - var id = this.id; - util.forEach(this.listeners, function (callback, event) { - me.groupsData.subscribe(event, callback, id); - }); + // remove all drawn groups + ids = this.groupsData.getIds(); + this._onRemove(ids); + } + + // replace the dataset + if (!groups) { + this.groupsData = null; + } + else if (groups instanceof DataSet) { + this.groupsData = groups; + } + else { + this.groupsData = new DataSet({ + convert: { + start: 'Date', + end: 'Date' + } + }); + this.groupsData.add(groups); + } + + if (this.groupsData) { + // subscribe to new dataset + var id = this.id; + util.forEach(this.listeners, function (callback, event) { + me.groupsData.subscribe(event, callback, id); + }); - // draw all new groups - ids = this.groupsData.getIds(); - this._onAdd(ids); - } + // draw all new groups + ids = this.groupsData.getIds(); + this._onAdd(ids); + } }; /** @@ -146,7 +146,7 @@ GroupSet.prototype.setGroups = function setGroups(groups) { * @return {vis.DataSet | null} groups */ GroupSet.prototype.getGroups = function getGroups() { - return this.groupsData; + return this.groupsData; }; /** @@ -154,179 +154,179 @@ GroupSet.prototype.getGroups = function getGroups() { * @return {Boolean} changed */ GroupSet.prototype.repaint = function repaint() { - var changed = 0, - i, id, group, label, - update = util.updateProperty, - asSize = util.option.asSize, - asElement = util.option.asElement, - options = this.options, - frame = this.dom.frame, - labels = this.dom.labels, - labelSet = this.dom.labelSet; - - // create frame - if (!this.parent) { - throw new Error('Cannot repaint groupset: no parent attached'); - } - var parentContainer = this.parent.getContainer(); - if (!parentContainer) { - throw new Error('Cannot repaint groupset: parent has no container element'); - } - if (!frame) { - frame = document.createElement('div'); - frame.className = 'groupset'; - this.dom.frame = frame; - - var className = options.className; - if (className) { - util.addClassName(frame, util.option.asString(className)); - } - - changed += 1; - } - if (!frame.parentNode) { - parentContainer.appendChild(frame); - changed += 1; + var changed = 0, + i, id, group, label, + update = util.updateProperty, + asSize = util.option.asSize, + asElement = util.option.asElement, + options = this.options, + frame = this.dom.frame, + labels = this.dom.labels, + labelSet = this.dom.labelSet; + + // create frame + if (!this.parent) { + throw new Error('Cannot repaint groupset: no parent attached'); + } + var parentContainer = this.parent.getContainer(); + if (!parentContainer) { + throw new Error('Cannot repaint groupset: parent has no container element'); + } + if (!frame) { + frame = document.createElement('div'); + frame.className = 'groupset'; + this.dom.frame = frame; + + var className = options.className; + if (className) { + util.addClassName(frame, util.option.asString(className)); } - // create labels - var labelContainer = asElement(options.labelContainer); - if (!labelContainer) { - throw new Error('Cannot repaint groupset: option "labelContainer" not defined'); + changed += 1; + } + if (!frame.parentNode) { + parentContainer.appendChild(frame); + changed += 1; + } + + // create labels + var labelContainer = asElement(options.labelContainer); + if (!labelContainer) { + throw new Error('Cannot repaint groupset: option "labelContainer" not defined'); + } + if (!labels) { + labels = document.createElement('div'); + labels.className = 'labels'; + this.dom.labels = labels; + } + if (!labelSet) { + labelSet = document.createElement('div'); + labelSet.className = 'label-set'; + labels.appendChild(labelSet); + this.dom.labelSet = labelSet; + } + if (!labels.parentNode || labels.parentNode != labelContainer) { + if (labels.parentNode) { + labels.parentNode.removeChild(labels.parentNode); } - if (!labels) { - labels = document.createElement('div'); - labels.className = 'labels'; - this.dom.labels = labels; - } - if (!labelSet) { - labelSet = document.createElement('div'); - labelSet.className = 'label-set'; - labels.appendChild(labelSet); - this.dom.labelSet = labelSet; - } - if (!labels.parentNode || labels.parentNode != labelContainer) { - if (labels.parentNode) { - labels.parentNode.removeChild(labels.parentNode); - } - labelContainer.appendChild(labels); - } - - // reposition frame - changed += update(frame.style, 'height', asSize(options.height, this.height + 'px')); - changed += update(frame.style, 'top', asSize(options.top, '0px')); - changed += update(frame.style, 'left', asSize(options.left, '0px')); - changed += update(frame.style, 'width', asSize(options.width, '100%')); - - // reposition labels - changed += update(labelSet.style, 'top', asSize(options.top, '0px')); - changed += update(labelSet.style, 'height', asSize(options.height, this.height + 'px')); - - var me = this, - queue = this.queue, - groups = this.groups, - groupsData = this.groupsData; - - // show/hide added/changed/removed groups - var ids = Object.keys(queue); - if (ids.length) { - ids.forEach(function (id) { - var action = queue[id]; - var group = groups[id]; - - //noinspection FallthroughInSwitchStatementJS - switch (action) { - case 'add': - case 'update': - if (!group) { - var groupOptions = Object.create(me.options); - util.extend(groupOptions, { - height: null, - maxHeight: null - }); - - group = new Group(me, id, groupOptions); - group.setItems(me.itemsData); // attach items data - groups[id] = group; - - me.controller.add(group); - } - - // TODO: update group data - group.data = groupsData.get(id); - - delete queue[id]; - break; - - case 'remove': - if (group) { - group.setItems(); // detach items data - delete groups[id]; - - me.controller.remove(group); - } - - // update lists - delete queue[id]; - break; - - default: - console.log('Error: unknown action "' + action + '"'); - } - }); - - // the groupset depends on each of the groups - //this.depends = this.groups; // TODO: gives a circular reference through the parent + labelContainer.appendChild(labels); + } + + // reposition frame + changed += update(frame.style, 'height', asSize(options.height, this.height + 'px')); + changed += update(frame.style, 'top', asSize(options.top, '0px')); + changed += update(frame.style, 'left', asSize(options.left, '0px')); + changed += update(frame.style, 'width', asSize(options.width, '100%')); + + // reposition labels + changed += update(labelSet.style, 'top', asSize(options.top, '0px')); + changed += update(labelSet.style, 'height', asSize(options.height, this.height + 'px')); + + var me = this, + queue = this.queue, + groups = this.groups, + groupsData = this.groupsData; + + // show/hide added/changed/removed groups + var ids = Object.keys(queue); + if (ids.length) { + ids.forEach(function (id) { + var action = queue[id]; + var group = groups[id]; + + //noinspection FallthroughInSwitchStatementJS + switch (action) { + case 'add': + case 'update': + if (!group) { + var groupOptions = Object.create(me.options); + util.extend(groupOptions, { + height: null, + maxHeight: null + }); + + group = new Group(me, id, groupOptions); + group.setItems(me.itemsData); // attach items data + groups[id] = group; + + me.controller.add(group); + } + + // TODO: update group data + group.data = groupsData.get(id); + + delete queue[id]; + break; + + case 'remove': + if (group) { + group.setItems(); // detach items data + delete groups[id]; + + me.controller.remove(group); + } + + // update lists + delete queue[id]; + break; + + default: + console.log('Error: unknown action "' + action + '"'); + } + }); - // TODO: apply dependencies of the groupset + // the groupset depends on each of the groups + //this.depends = this.groups; // TODO: gives a circular reference through the parent - // update the top positions of the groups in the correct order - var orderedGroups = this.groupsData.getIds({ - order: this.options.groupOrder - }); - for (i = 0; i < orderedGroups.length; i++) { - (function (group, prevGroup) { - var top = 0; - if (prevGroup) { - top = function () { - // TODO: top must reckon with options.maxHeight - return prevGroup.top + prevGroup.height; - } - } - group.setOptions({ - top: top - }); - })(groups[orderedGroups[i]], groups[orderedGroups[i - 1]]); - } + // TODO: apply dependencies of the groupset - // (re)create the labels - while (labelSet.firstChild) { - labelSet.removeChild(labelSet.firstChild); - } - for (i = 0; i < orderedGroups.length; i++) { - id = orderedGroups[i]; - label = this._createLabel(id); - labelSet.appendChild(label); + // update the top positions of the groups in the correct order + var orderedGroups = this.groupsData.getIds({ + order: this.options.groupOrder + }); + for (i = 0; i < orderedGroups.length; i++) { + (function (group, prevGroup) { + var top = 0; + if (prevGroup) { + top = function () { + // TODO: top must reckon with options.maxHeight + return prevGroup.top + prevGroup.height; + } } + group.setOptions({ + top: top + }); + })(groups[orderedGroups[i]], groups[orderedGroups[i - 1]]); + } - changed++; + // (re)create the labels + while (labelSet.firstChild) { + labelSet.removeChild(labelSet.firstChild); + } + for (i = 0; i < orderedGroups.length; i++) { + id = orderedGroups[i]; + label = this._createLabel(id); + labelSet.appendChild(label); } - // reposition the labels - // TODO: labels are not displayed correctly when orientation=='top' - // TODO: width of labelPanel is not immediately updated on a change in groups - for (id in groups) { - if (groups.hasOwnProperty(id)) { - group = groups[id]; - label = group.label; - if (label) { - label.style.top = group.top + 'px'; - label.style.height = group.height + 'px'; - } - } + changed++; + } + + // reposition the labels + // TODO: labels are not displayed correctly when orientation=='top' + // TODO: width of labelPanel is not immediately updated on a change in groups + for (id in groups) { + if (groups.hasOwnProperty(id)) { + group = groups[id]; + label = group.label; + if (label) { + label.style.top = group.top + 'px'; + label.style.height = group.height + 'px'; + } } + } - return (changed > 0); + return (changed > 0); }; /** @@ -336,29 +336,29 @@ GroupSet.prototype.repaint = function repaint() { * @private */ GroupSet.prototype._createLabel = function(id) { - var group = this.groups[id]; - var label = document.createElement('div'); - label.className = 'label'; - var inner = document.createElement('div'); - inner.className = 'inner'; - label.appendChild(inner); - - var content = group.data && group.data.content; - if (content instanceof Element) { - inner.appendChild(content); - } - else if (content != undefined) { - inner.innerHTML = content; - } - - var className = group.data && group.data.className; - if (className) { - util.addClassName(label, className); - } - - group.label = label; // TODO: not so nice, parking labels in the group this way!!! - - return label; + var group = this.groups[id]; + var label = document.createElement('div'); + label.className = 'label'; + var inner = document.createElement('div'); + inner.className = 'inner'; + label.appendChild(inner); + + var content = group.data && group.data.content; + if (content instanceof Element) { + inner.appendChild(content); + } + else if (content != undefined) { + inner.innerHTML = content; + } + + var className = group.data && group.data.className; + if (className) { + util.addClassName(label, className); + } + + group.label = label; // TODO: not so nice, parking labels in the group this way!!! + + return label; }; /** @@ -366,7 +366,7 @@ GroupSet.prototype._createLabel = function(id) { * @return {HTMLElement} container */ GroupSet.prototype.getContainer = function getContainer() { - return this.dom.frame; + return this.dom.frame; }; /** @@ -374,7 +374,7 @@ GroupSet.prototype.getContainer = function getContainer() { * @return {Number} width */ GroupSet.prototype.getLabelsWidth = function getContainer() { - return this.props.labels.width; + return this.props.labels.width; }; /** @@ -382,54 +382,54 @@ GroupSet.prototype.getLabelsWidth = function getContainer() { * @return {Boolean} resized */ GroupSet.prototype.reflow = function reflow() { - var changed = 0, - id, group, - options = this.options, - update = util.updateProperty, - asNumber = util.option.asNumber, - asSize = util.option.asSize, - frame = this.dom.frame; - - if (frame) { - var maxHeight = asNumber(options.maxHeight); - var fixedHeight = (asSize(options.height) != null); - var height; - if (fixedHeight) { - height = frame.offsetHeight; - } - else { - // height is not specified, calculate the sum of the height of all groups - height = 0; - - for (id in this.groups) { - if (this.groups.hasOwnProperty(id)) { - group = this.groups[id]; - height += group.height; - } - } - } - if (maxHeight != null) { - height = Math.min(height, maxHeight); - } - changed += update(this, 'height', height); - - changed += update(this, 'top', frame.offsetTop); - changed += update(this, 'left', frame.offsetLeft); - changed += update(this, 'width', frame.offsetWidth); + var changed = 0, + id, group, + options = this.options, + update = util.updateProperty, + asNumber = util.option.asNumber, + asSize = util.option.asSize, + frame = this.dom.frame; + + if (frame) { + var maxHeight = asNumber(options.maxHeight); + var fixedHeight = (asSize(options.height) != null); + var height; + if (fixedHeight) { + height = frame.offsetHeight; } + else { + // height is not specified, calculate the sum of the height of all groups + height = 0; - // calculate the maximum width of the labels - var width = 0; - for (id in this.groups) { + for (id in this.groups) { if (this.groups.hasOwnProperty(id)) { - group = this.groups[id]; - var labelWidth = group.props && group.props.label && group.props.label.width || 0; - width = Math.max(width, labelWidth); + group = this.groups[id]; + height += group.height; } + } } - changed += update(this.props.labels, 'width', width); + if (maxHeight != null) { + height = Math.min(height, maxHeight); + } + changed += update(this, 'height', height); + + changed += update(this, 'top', frame.offsetTop); + changed += update(this, 'left', frame.offsetLeft); + changed += update(this, 'width', frame.offsetWidth); + } + + // calculate the maximum width of the labels + var width = 0; + for (id in this.groups) { + if (this.groups.hasOwnProperty(id)) { + group = this.groups[id]; + var labelWidth = group.props && group.props.label && group.props.label.width || 0; + width = Math.max(width, labelWidth); + } + } + changed += update(this.props.labels, 'width', width); - return (changed > 0); + return (changed > 0); }; /** @@ -437,13 +437,13 @@ GroupSet.prototype.reflow = function reflow() { * @return {Boolean} changed */ GroupSet.prototype.hide = function hide() { - if (this.dom.frame && this.dom.frame.parentNode) { - this.dom.frame.parentNode.removeChild(this.dom.frame); - return true; - } - else { - return false; - } + if (this.dom.frame && this.dom.frame.parentNode) { + this.dom.frame.parentNode.removeChild(this.dom.frame); + return true; + } + else { + return false; + } }; /** @@ -452,12 +452,12 @@ GroupSet.prototype.hide = function hide() { * @return {Boolean} changed */ GroupSet.prototype.show = function show() { - if (!this.dom.frame || !this.dom.frame.parentNode) { - return this.repaint(); - } - else { - return false; - } + if (!this.dom.frame || !this.dom.frame.parentNode) { + return this.repaint(); + } + else { + return false; + } }; /** @@ -466,7 +466,7 @@ GroupSet.prototype.show = function show() { * @private */ GroupSet.prototype._onUpdate = function _onUpdate(ids) { - this._toQueue(ids, 'update'); + this._toQueue(ids, 'update'); }; /** @@ -475,7 +475,7 @@ GroupSet.prototype._onUpdate = function _onUpdate(ids) { * @private */ GroupSet.prototype._onAdd = function _onAdd(ids) { - this._toQueue(ids, 'add'); + this._toQueue(ids, 'add'); }; /** @@ -484,7 +484,7 @@ GroupSet.prototype._onAdd = function _onAdd(ids) { * @private */ GroupSet.prototype._onRemove = function _onRemove(ids) { - this._toQueue(ids, 'remove'); + this._toQueue(ids, 'remove'); }; /** @@ -493,13 +493,13 @@ GroupSet.prototype._onRemove = function _onRemove(ids) { * @param {String} action can be 'add', 'update', 'remove' */ GroupSet.prototype._toQueue = function _toQueue(ids, action) { - var queue = this.queue; - ids.forEach(function (id) { - queue[id] = action; - }); - - if (this.controller) { - //this.requestReflow(); - this.requestRepaint(); - } + var queue = this.queue; + ids.forEach(function (id) { + queue[id] = action; + }); + + if (this.controller) { + //this.requestReflow(); + this.requestRepaint(); + } }; diff --git a/src/timeline/component/ItemSet.js b/src/timeline/component/ItemSet.js index 6973a375..f68a3572 100644 --- a/src/timeline/component/ItemSet.js +++ b/src/timeline/component/ItemSet.js @@ -12,63 +12,63 @@ */ // TODO: improve performance by replacing all Array.forEach with a for loop function ItemSet(parent, depends, options) { - this.id = util.randomUUID(); - this.parent = parent; - this.depends = depends; - - // one options object is shared by this itemset and all its items - this.options = options || {}; - this.defaultOptions = { - type: 'box', - align: 'center', - orientation: 'bottom', - margin: { - axis: 20, - item: 10 - }, - padding: 5 - }; - - this.dom = {}; - - var me = this; - this.itemsData = null; // DataSet - this.range = null; // Range or Object {start: number, end: number} - - this.listeners = { - 'add': function (event, params, senderId) { - if (senderId != me.id) { - me._onAdd(params.items); - } - }, - 'update': function (event, params, senderId) { - if (senderId != me.id) { - me._onUpdate(params.items); - } - }, - 'remove': function (event, params, senderId) { - if (senderId != me.id) { - me._onRemove(params.items); - } - } - }; + this.id = util.randomUUID(); + this.parent = parent; + this.depends = depends; + + // one options object is shared by this itemset and all its items + this.options = options || {}; + this.defaultOptions = { + type: 'box', + align: 'center', + orientation: 'bottom', + margin: { + axis: 20, + item: 10 + }, + padding: 5 + }; + + this.dom = {}; + + var me = this; + this.itemsData = null; // DataSet + this.range = null; // Range or Object {start: number, end: number} + + this.listeners = { + 'add': function (event, params, senderId) { + if (senderId != me.id) { + me._onAdd(params.items); + } + }, + 'update': function (event, params, senderId) { + if (senderId != me.id) { + me._onUpdate(params.items); + } + }, + 'remove': function (event, params, senderId) { + if (senderId != me.id) { + me._onRemove(params.items); + } + } + }; - this.items = {}; // object with an Item for every data item - this.queue = {}; // queue with id/actions: 'add', 'update', 'delete' - this.stack = new Stack(this, Object.create(this.options)); - this.conversion = null; + this.items = {}; // object with an Item for every data item + this.queue = {}; // queue with id/actions: 'add', 'update', 'delete' + this.stack = new Stack(this, Object.create(this.options)); + this.conversion = null; - // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis + // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis } ItemSet.prototype = new Panel(); // available item types will be registered here ItemSet.types = { - box: ItemBox, - range: ItemRange, - rangeoverflow: ItemRangeOverflow, - point: ItemPoint + box: ItemBox, + range: ItemRange, + rangeoverflow: ItemRangeOverflow, + point: ItemPoint }; /** @@ -103,11 +103,11 @@ ItemSet.prototype.setOptions = Component.prototype.setOptions; * @param {Range | Object} range A Range or an object containing start and end. */ ItemSet.prototype.setRange = function setRange(range) { - if (!(range instanceof Range) && (!range || !range.start || !range.end)) { - throw new TypeError('Range must be an instance of Range, ' + - 'or an object containing start and end.'); - } - this.range = range; + if (!(range instanceof Range) && (!range || !range.start || !range.end)) { + throw new TypeError('Range must be an instance of Range, ' + + 'or an object containing start and end.'); + } + this.range = range; }; /** @@ -115,170 +115,170 @@ ItemSet.prototype.setRange = function setRange(range) { * @return {Boolean} changed */ ItemSet.prototype.repaint = function repaint() { - var changed = 0, - update = util.updateProperty, - asSize = util.option.asSize, - options = this.options, - orientation = this.getOption('orientation'), - defaultOptions = this.defaultOptions, - frame = this.frame; - - if (!frame) { - frame = document.createElement('div'); - frame.className = 'itemset'; - - var className = options.className; - if (className) { - util.addClassName(frame, util.option.asString(className)); - } - - // create background panel - var background = document.createElement('div'); - background.className = 'background'; - frame.appendChild(background); - this.dom.background = background; - - // create foreground panel - var foreground = document.createElement('div'); - foreground.className = 'foreground'; - frame.appendChild(foreground); - this.dom.foreground = foreground; - - // create axis panel - var axis = document.createElement('div'); - axis.className = 'itemset-axis'; - //frame.appendChild(axis); - this.dom.axis = axis; - - this.frame = frame; - changed += 1; + var changed = 0, + update = util.updateProperty, + asSize = util.option.asSize, + options = this.options, + orientation = this.getOption('orientation'), + defaultOptions = this.defaultOptions, + frame = this.frame; + + if (!frame) { + frame = document.createElement('div'); + frame.className = 'itemset'; + + var className = options.className; + if (className) { + util.addClassName(frame, util.option.asString(className)); } - if (!this.parent) { - throw new Error('Cannot repaint itemset: no parent attached'); - } - var parentContainer = this.parent.getContainer(); - if (!parentContainer) { - throw new Error('Cannot repaint itemset: parent has no container element'); - } - if (!frame.parentNode) { - parentContainer.appendChild(frame); - changed += 1; - } - if (!this.dom.axis.parentNode) { - parentContainer.appendChild(this.dom.axis); - changed += 1; - } + // create background panel + var background = document.createElement('div'); + background.className = 'background'; + frame.appendChild(background); + this.dom.background = background; + + // create foreground panel + var foreground = document.createElement('div'); + foreground.className = 'foreground'; + frame.appendChild(foreground); + this.dom.foreground = foreground; + + // create axis panel + var axis = document.createElement('div'); + axis.className = 'itemset-axis'; + //frame.appendChild(axis); + this.dom.axis = axis; + + this.frame = frame; + changed += 1; + } + + if (!this.parent) { + throw new Error('Cannot repaint itemset: no parent attached'); + } + var parentContainer = this.parent.getContainer(); + if (!parentContainer) { + throw new Error('Cannot repaint itemset: parent has no container element'); + } + if (!frame.parentNode) { + parentContainer.appendChild(frame); + changed += 1; + } + if (!this.dom.axis.parentNode) { + parentContainer.appendChild(this.dom.axis); + changed += 1; + } + + // reposition frame + changed += update(frame.style, 'left', asSize(options.left, '0px')); + changed += update(frame.style, 'top', asSize(options.top, '0px')); + changed += update(frame.style, 'width', asSize(options.width, '100%')); + changed += update(frame.style, 'height', asSize(options.height, this.height + 'px')); + + // reposition axis + changed += update(this.dom.axis.style, 'left', asSize(options.left, '0px')); + changed += update(this.dom.axis.style, 'width', asSize(options.width, '100%')); + if (orientation == 'bottom') { + changed += update(this.dom.axis.style, 'top', (this.height + this.top) + 'px'); + } + else { // orientation == 'top' + changed += update(this.dom.axis.style, 'top', this.top + 'px'); + } + + this._updateConversion(); + + var me = this, + queue = this.queue, + itemsData = this.itemsData, + items = this.items, + dataOptions = { + // TODO: cleanup + // fields: [(itemsData && itemsData.fieldId || 'id'), 'start', 'end', 'content', 'type', 'className'] + }; + + // show/hide added/changed/removed items + Object.keys(queue).forEach(function (id) { + //var entry = queue[id]; + var action = queue[id]; + var item = items[id]; + //var item = entry.item; + //noinspection FallthroughInSwitchStatementJS + switch (action) { + case 'add': + case 'update': + var itemData = itemsData && itemsData.get(id, dataOptions); + + if (itemData) { + var type = itemData.type || + (itemData.start && itemData.end && 'range') || + options.type || + 'box'; + var constructor = ItemSet.types[type]; + + // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error? + if (item) { + // update item + if (!constructor || !(item instanceof constructor)) { + // item type has changed, hide and delete the item + changed += item.hide(); + item = null; + } + else { + item.data = itemData; // TODO: create a method item.setData ? + changed++; + } + } - // reposition frame - changed += update(frame.style, 'left', asSize(options.left, '0px')); - changed += update(frame.style, 'top', asSize(options.top, '0px')); - changed += update(frame.style, 'width', asSize(options.width, '100%')); - changed += update(frame.style, 'height', asSize(options.height, this.height + 'px')); - - // reposition axis - changed += update(this.dom.axis.style, 'left', asSize(options.left, '0px')); - changed += update(this.dom.axis.style, 'width', asSize(options.width, '100%')); - if (orientation == 'bottom') { - changed += update(this.dom.axis.style, 'top', (this.height + this.top) + 'px'); - } - else { // orientation == 'top' - changed += update(this.dom.axis.style, 'top', this.top + 'px'); - } + if (!item) { + // create item + if (constructor) { + item = new constructor(me, itemData, options, defaultOptions); + changed++; + } + else { + throw new TypeError('Unknown item type "' + type + '"'); + } + } - this._updateConversion(); + // force a repaint (not only a reposition) + item.repaint(); - var me = this, - queue = this.queue, - itemsData = this.itemsData, - items = this.items, - dataOptions = { - // TODO: cleanup - // fields: [(itemsData && itemsData.fieldId || 'id'), 'start', 'end', 'content', 'type', 'className'] - }; - - // show/hide added/changed/removed items - Object.keys(queue).forEach(function (id) { - //var entry = queue[id]; - var action = queue[id]; - var item = items[id]; - //var item = entry.item; - //noinspection FallthroughInSwitchStatementJS - switch (action) { - case 'add': - case 'update': - var itemData = itemsData && itemsData.get(id, dataOptions); - - if (itemData) { - var type = itemData.type || - (itemData.start && itemData.end && 'range') || - options.type || - 'box'; - var constructor = ItemSet.types[type]; - - // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error? - if (item) { - // update item - if (!constructor || !(item instanceof constructor)) { - // item type has changed, hide and delete the item - changed += item.hide(); - item = null; - } - else { - item.data = itemData; // TODO: create a method item.setData ? - changed++; - } - } - - if (!item) { - // create item - if (constructor) { - item = new constructor(me, itemData, options, defaultOptions); - changed++; - } - else { - throw new TypeError('Unknown item type "' + type + '"'); - } - } - - // force a repaint (not only a reposition) - item.repaint(); - - items[id] = item; - } - - // update queue - delete queue[id]; - break; - - case 'remove': - if (item) { - // remove DOM of the item - changed += item.hide(); - } - - // update lists - delete items[id]; - delete queue[id]; - break; - - default: - console.log('Error: unknown action "' + action + '"'); + items[id] = item; } - }); - // reposition all items. Show items only when in the visible area - util.forEach(this.items, function (item) { - if (item.visible) { - changed += item.show(); - item.reposition(); - } - else { - changed += item.hide(); + // update queue + delete queue[id]; + break; + + case 'remove': + if (item) { + // remove DOM of the item + changed += item.hide(); } - }); - return (changed > 0); + // update lists + delete items[id]; + delete queue[id]; + break; + + default: + console.log('Error: unknown action "' + action + '"'); + } + }); + + // reposition all items. Show items only when in the visible area + util.forEach(this.items, function (item) { + if (item.visible) { + changed += item.show(); + item.reposition(); + } + else { + changed += item.hide(); + } + }); + + return (changed > 0); }; /** @@ -286,7 +286,7 @@ ItemSet.prototype.repaint = function repaint() { * @return {HTMLElement} foreground */ ItemSet.prototype.getForeground = function getForeground() { - return this.dom.foreground; + return this.dom.foreground; }; /** @@ -294,7 +294,7 @@ ItemSet.prototype.getForeground = function getForeground() { * @return {HTMLElement} background */ ItemSet.prototype.getBackground = function getBackground() { - return this.dom.background; + return this.dom.background; }; /** @@ -302,7 +302,7 @@ ItemSet.prototype.getBackground = function getBackground() { * @return {HTMLElement} axis */ ItemSet.prototype.getAxis = function getAxis() { - return this.dom.axis; + return this.dom.axis; }; /** @@ -310,63 +310,63 @@ ItemSet.prototype.getAxis = function getAxis() { * @return {Boolean} resized */ ItemSet.prototype.reflow = function reflow () { - var changed = 0, - options = this.options, - marginAxis = options.margin && options.margin.axis || this.defaultOptions.margin.axis, - marginItem = options.margin && options.margin.item || this.defaultOptions.margin.item, - update = util.updateProperty, - asNumber = util.option.asNumber, - asSize = util.option.asSize, - frame = this.frame; - - if (frame) { - this._updateConversion(); - - util.forEach(this.items, function (item) { - changed += item.reflow(); - }); + var changed = 0, + options = this.options, + marginAxis = options.margin && options.margin.axis || this.defaultOptions.margin.axis, + marginItem = options.margin && options.margin.item || this.defaultOptions.margin.item, + update = util.updateProperty, + asNumber = util.option.asNumber, + asSize = util.option.asSize, + frame = this.frame; + + if (frame) { + this._updateConversion(); - // TODO: stack.update should be triggered via an event, in stack itself - // TODO: only update the stack when there are changed items - this.stack.update(); + util.forEach(this.items, function (item) { + changed += item.reflow(); + }); - var maxHeight = asNumber(options.maxHeight); - var fixedHeight = (asSize(options.height) != null); - var height; - if (fixedHeight) { - height = frame.offsetHeight; - } - else { - // height is not specified, determine the height from the height and positioned items - var visibleItems = this.stack.ordered; // TODO: not so nice way to get the filtered items - if (visibleItems.length) { - var min = visibleItems[0].top; - var max = visibleItems[0].top + visibleItems[0].height; - util.forEach(visibleItems, function (item) { - min = Math.min(min, item.top); - max = Math.max(max, (item.top + item.height)); - }); - height = (max - min) + marginAxis + marginItem; - } - else { - height = marginAxis + marginItem; - } - } - if (maxHeight != null) { - height = Math.min(height, maxHeight); - } - changed += update(this, 'height', height); + // TODO: stack.update should be triggered via an event, in stack itself + // TODO: only update the stack when there are changed items + this.stack.update(); - // calculate height from items - changed += update(this, 'top', frame.offsetTop); - changed += update(this, 'left', frame.offsetLeft); - changed += update(this, 'width', frame.offsetWidth); + var maxHeight = asNumber(options.maxHeight); + var fixedHeight = (asSize(options.height) != null); + var height; + if (fixedHeight) { + height = frame.offsetHeight; } else { - changed += 1; + // height is not specified, determine the height from the height and positioned items + var visibleItems = this.stack.ordered; // TODO: not so nice way to get the filtered items + if (visibleItems.length) { + var min = visibleItems[0].top; + var max = visibleItems[0].top + visibleItems[0].height; + util.forEach(visibleItems, function (item) { + min = Math.min(min, item.top); + max = Math.max(max, (item.top + item.height)); + }); + height = (max - min) + marginAxis + marginItem; + } + else { + height = marginAxis + marginItem; + } } - - return (changed > 0); + if (maxHeight != null) { + height = Math.min(height, maxHeight); + } + changed += update(this, 'height', height); + + // calculate height from items + changed += update(this, 'top', frame.offsetTop); + changed += update(this, 'left', frame.offsetLeft); + changed += update(this, 'width', frame.offsetWidth); + } + else { + changed += 1; + } + + return (changed > 0); }; /** @@ -374,19 +374,19 @@ ItemSet.prototype.reflow = function reflow () { * @return {Boolean} changed */ ItemSet.prototype.hide = function hide() { - var changed = false; - - // remove the DOM - if (this.frame && this.frame.parentNode) { - this.frame.parentNode.removeChild(this.frame); - changed = true; - } - if (this.dom.axis && this.dom.axis.parentNode) { - this.dom.axis.parentNode.removeChild(this.dom.axis); - changed = true; - } - - return changed; + var changed = false; + + // remove the DOM + if (this.frame && this.frame.parentNode) { + this.frame.parentNode.removeChild(this.frame); + changed = true; + } + if (this.dom.axis && this.dom.axis.parentNode) { + this.dom.axis.parentNode.removeChild(this.dom.axis); + changed = true; + } + + return changed; }; /** @@ -394,43 +394,43 @@ ItemSet.prototype.hide = function hide() { * @param {vis.DataSet | null} items */ ItemSet.prototype.setItems = function setItems(items) { - var me = this, - ids, - oldItemsData = this.itemsData; - - // replace the dataset - if (!items) { - this.itemsData = null; - } - else if (items instanceof DataSet || items instanceof DataView) { - this.itemsData = items; - } - else { - throw new TypeError('Data must be an instance of DataSet'); - } + var me = this, + ids, + oldItemsData = this.itemsData; + + // replace the dataset + if (!items) { + this.itemsData = null; + } + else if (items instanceof DataSet || items instanceof DataView) { + this.itemsData = items; + } + else { + throw new TypeError('Data must be an instance of DataSet'); + } + + if (oldItemsData) { + // unsubscribe from old dataset + util.forEach(this.listeners, function (callback, event) { + oldItemsData.unsubscribe(event, callback); + }); - if (oldItemsData) { - // unsubscribe from old dataset - util.forEach(this.listeners, function (callback, event) { - oldItemsData.unsubscribe(event, callback); - }); + // remove all drawn items + ids = oldItemsData.getIds(); + this._onRemove(ids); + } - // remove all drawn items - ids = oldItemsData.getIds(); - this._onRemove(ids); - } - - if (this.itemsData) { - // subscribe to new dataset - var id = this.id; - util.forEach(this.listeners, function (callback, event) { - me.itemsData.subscribe(event, callback, id); - }); + if (this.itemsData) { + // subscribe to new dataset + var id = this.id; + util.forEach(this.listeners, function (callback, event) { + me.itemsData.subscribe(event, callback, id); + }); - // draw all new items - ids = this.itemsData.getIds(); - this._onAdd(ids); - } + // draw all new items + ids = this.itemsData.getIds(); + this._onAdd(ids); + } }; /** @@ -438,7 +438,7 @@ ItemSet.prototype.setItems = function setItems(items) { * @returns {vis.DataSet | null} */ ItemSet.prototype.getItems = function getItems() { - return this.itemsData; + return this.itemsData; }; /** @@ -447,7 +447,7 @@ ItemSet.prototype.getItems = function getItems() { * @private */ ItemSet.prototype._onUpdate = function _onUpdate(ids) { - this._toQueue('update', ids); + this._toQueue('update', ids); }; /** @@ -456,7 +456,7 @@ ItemSet.prototype._onUpdate = function _onUpdate(ids) { * @private */ ItemSet.prototype._onAdd = function _onAdd(ids) { - this._toQueue('add', ids); + this._toQueue('add', ids); }; /** @@ -465,7 +465,7 @@ ItemSet.prototype._onAdd = function _onAdd(ids) { * @private */ ItemSet.prototype._onRemove = function _onRemove(ids) { - this._toQueue('remove', ids); + this._toQueue('remove', ids); }; /** @@ -474,15 +474,15 @@ ItemSet.prototype._onRemove = function _onRemove(ids) { * @param {Number[]} ids */ ItemSet.prototype._toQueue = function _toQueue(action, ids) { - var queue = this.queue; - ids.forEach(function (id) { - queue[id] = action; - }); - - if (this.controller) { - //this.requestReflow(); - this.requestRepaint(); - } + var queue = this.queue; + ids.forEach(function (id) { + queue[id] = action; + }); + + if (this.controller) { + //this.requestReflow(); + this.requestRepaint(); + } }; /** @@ -493,17 +493,17 @@ ItemSet.prototype._toQueue = function _toQueue(action, ids) { * @private */ ItemSet.prototype._updateConversion = function _updateConversion() { - var range = this.range; - if (!range) { - throw new Error('No range configured'); - } - - if (range.conversion) { - this.conversion = range.conversion(this.width); - } - else { - this.conversion = Range.conversion(range.start, range.end, this.width); - } + var range = this.range; + if (!range) { + throw new Error('No range configured'); + } + + if (range.conversion) { + this.conversion = range.conversion(this.width); + } + else { + this.conversion = Range.conversion(range.start, range.end, this.width); + } }; /** @@ -514,8 +514,8 @@ ItemSet.prototype._updateConversion = function _updateConversion() { * @return {Date} time The datetime the corresponds with given position x */ ItemSet.prototype.toTime = function toTime(x) { - var conversion = this.conversion; - return new Date(x / conversion.scale + conversion.offset); + var conversion = this.conversion; + return new Date(x / conversion.scale + conversion.offset); }; /** @@ -527,6 +527,6 @@ ItemSet.prototype.toTime = function toTime(x) { * with the given date. */ ItemSet.prototype.toScreen = function toScreen(time) { - var conversion = this.conversion; - return (time.valueOf() - conversion.offset) * conversion.scale; + var conversion = this.conversion; + return (time.valueOf() - conversion.offset) * conversion.scale; }; diff --git a/src/timeline/component/Panel.js b/src/timeline/component/Panel.js index 53b41412..e3badfae 100644 --- a/src/timeline/component/Panel.js +++ b/src/timeline/component/Panel.js @@ -13,11 +13,11 @@ * @extends Component */ function Panel(parent, depends, options) { - this.id = util.randomUUID(); - this.parent = parent; - this.depends = depends; + this.id = util.randomUUID(); + this.parent = parent; + this.depends = depends; - this.options = options || {}; + this.options = options || {}; } Panel.prototype = new Component(); @@ -39,7 +39,7 @@ Panel.prototype.setOptions = Component.prototype.setOptions; * @returns {HTMLElement} container */ Panel.prototype.getContainer = function () { - return this.frame; + return this.frame; }; /** @@ -47,46 +47,46 @@ Panel.prototype.getContainer = function () { * @return {Boolean} changed */ Panel.prototype.repaint = function () { - var changed = 0, - update = util.updateProperty, - asSize = util.option.asSize, - options = this.options, - frame = this.frame; - if (!frame) { - frame = document.createElement('div'); - frame.className = 'panel'; + var changed = 0, + update = util.updateProperty, + asSize = util.option.asSize, + options = this.options, + frame = this.frame; + if (!frame) { + frame = document.createElement('div'); + frame.className = 'panel'; - var className = options.className; - if (className) { - if (typeof className == 'function') { - util.addClassName(frame, String(className())); - } - else { - util.addClassName(frame, String(className)); - } - } + var className = options.className; + if (className) { + if (typeof className == 'function') { + util.addClassName(frame, String(className())); + } + else { + util.addClassName(frame, String(className)); + } + } - this.frame = frame; - changed += 1; + this.frame = frame; + changed += 1; + } + if (!frame.parentNode) { + if (!this.parent) { + throw new Error('Cannot repaint panel: no parent attached'); } - if (!frame.parentNode) { - if (!this.parent) { - throw new Error('Cannot repaint panel: no parent attached'); - } - var parentContainer = this.parent.getContainer(); - if (!parentContainer) { - throw new Error('Cannot repaint panel: parent has no container element'); - } - parentContainer.appendChild(frame); - changed += 1; + var parentContainer = this.parent.getContainer(); + if (!parentContainer) { + throw new Error('Cannot repaint panel: parent has no container element'); } + parentContainer.appendChild(frame); + changed += 1; + } - changed += update(frame.style, 'top', asSize(options.top, '0px')); - changed += update(frame.style, 'left', asSize(options.left, '0px')); - changed += update(frame.style, 'width', asSize(options.width, '100%')); - changed += update(frame.style, 'height', asSize(options.height, '100%')); + changed += update(frame.style, 'top', asSize(options.top, '0px')); + changed += update(frame.style, 'left', asSize(options.left, '0px')); + changed += update(frame.style, 'width', asSize(options.width, '100%')); + changed += update(frame.style, 'height', asSize(options.height, '100%')); - return (changed > 0); + return (changed > 0); }; /** @@ -94,19 +94,19 @@ Panel.prototype.repaint = function () { * @return {Boolean} resized */ Panel.prototype.reflow = function () { - var changed = 0, - update = util.updateProperty, - frame = this.frame; + var changed = 0, + update = util.updateProperty, + frame = this.frame; - if (frame) { - changed += update(this, 'top', frame.offsetTop); - changed += update(this, 'left', frame.offsetLeft); - changed += update(this, 'width', frame.offsetWidth); - changed += update(this, 'height', frame.offsetHeight); - } - else { - changed += 1; - } + if (frame) { + changed += update(this, 'top', frame.offsetTop); + changed += update(this, 'left', frame.offsetLeft); + changed += update(this, 'width', frame.offsetWidth); + changed += update(this, 'height', frame.offsetHeight); + } + else { + changed += 1; + } - return (changed > 0); + return (changed > 0); }; diff --git a/src/timeline/component/RootPanel.js b/src/timeline/component/RootPanel.js index f55af08f..6bfd2db2 100644 --- a/src/timeline/component/RootPanel.js +++ b/src/timeline/component/RootPanel.js @@ -7,15 +7,15 @@ * @extends Panel */ function RootPanel(container, options) { - this.id = util.randomUUID(); - this.container = container; + this.id = util.randomUUID(); + this.container = container; - this.options = options || {}; - this.defaultOptions = { - autoResize: true - }; + this.options = options || {}; + this.defaultOptions = { + autoResize: true + }; - this.listeners = {}; // event listeners + this.listeners = {}; // event listeners } RootPanel.prototype = new Panel(); @@ -37,42 +37,42 @@ RootPanel.prototype.setOptions = Component.prototype.setOptions; * @return {Boolean} changed */ RootPanel.prototype.repaint = function () { - var changed = 0, - update = util.updateProperty, - asSize = util.option.asSize, - options = this.options, - frame = this.frame; - - if (!frame) { - frame = document.createElement('div'); - - this.frame = frame; - - changed += 1; - } - if (!frame.parentNode) { - if (!this.container) { - throw new Error('Cannot repaint root panel: no container attached'); - } - this.container.appendChild(frame); - changed += 1; + var changed = 0, + update = util.updateProperty, + asSize = util.option.asSize, + options = this.options, + frame = this.frame; + + if (!frame) { + frame = document.createElement('div'); + + this.frame = frame; + + changed += 1; + } + if (!frame.parentNode) { + if (!this.container) { + throw new Error('Cannot repaint root panel: no container attached'); } + this.container.appendChild(frame); + changed += 1; + } - frame.className = 'vis timeline rootpanel ' + options.orientation; - var className = options.className; - if (className) { - util.addClassName(frame, util.option.asString(className)); - } + frame.className = 'vis timeline rootpanel ' + options.orientation; + var className = options.className; + if (className) { + util.addClassName(frame, util.option.asString(className)); + } - changed += update(frame.style, 'top', asSize(options.top, '0px')); - changed += update(frame.style, 'left', asSize(options.left, '0px')); - changed += update(frame.style, 'width', asSize(options.width, '100%')); - changed += update(frame.style, 'height', asSize(options.height, '100%')); + changed += update(frame.style, 'top', asSize(options.top, '0px')); + changed += update(frame.style, 'left', asSize(options.left, '0px')); + changed += update(frame.style, 'width', asSize(options.width, '100%')); + changed += update(frame.style, 'height', asSize(options.height, '100%')); - this._updateEventEmitters(); - this._updateWatch(); + this._updateEventEmitters(); + this._updateWatch(); - return (changed > 0); + return (changed > 0); }; /** @@ -80,21 +80,21 @@ RootPanel.prototype.repaint = function () { * @return {Boolean} resized */ RootPanel.prototype.reflow = function () { - var changed = 0, - update = util.updateProperty, - frame = this.frame; - - if (frame) { - changed += update(this, 'top', frame.offsetTop); - changed += update(this, 'left', frame.offsetLeft); - changed += update(this, 'width', frame.offsetWidth); - changed += update(this, 'height', frame.offsetHeight); - } - else { - changed += 1; - } - - return (changed > 0); + var changed = 0, + update = util.updateProperty, + frame = this.frame; + + if (frame) { + changed += update(this, 'top', frame.offsetTop); + changed += update(this, 'left', frame.offsetLeft); + changed += update(this, 'width', frame.offsetWidth); + changed += update(this, 'height', frame.offsetHeight); + } + else { + changed += 1; + } + + return (changed > 0); }; /** @@ -102,13 +102,13 @@ RootPanel.prototype.reflow = function () { * @private */ RootPanel.prototype._updateWatch = function () { - var autoResize = this.getOption('autoResize'); - if (autoResize) { - this._watch(); - } - else { - this._unwatch(); - } + var autoResize = this.getOption('autoResize'); + if (autoResize) { + this._watch(); + } + else { + this._unwatch(); + } }; /** @@ -117,31 +117,31 @@ RootPanel.prototype._updateWatch = function () { * @private */ RootPanel.prototype._watch = function () { - var me = this; + var me = this; - this._unwatch(); + this._unwatch(); - var checkSize = function () { - var autoResize = me.getOption('autoResize'); - if (!autoResize) { - // stop watching when the option autoResize is changed to false - me._unwatch(); - return; - } + var checkSize = function () { + var autoResize = me.getOption('autoResize'); + if (!autoResize) { + // stop watching when the option autoResize is changed to false + me._unwatch(); + return; + } - if (me.frame) { - // check whether the frame is resized - if ((me.frame.clientWidth != me.width) || - (me.frame.clientHeight != me.height)) { - me.requestReflow(); - } - } - }; + if (me.frame) { + // check whether the frame is resized + if ((me.frame.clientWidth != me.width) || + (me.frame.clientHeight != me.height)) { + me.requestReflow(); + } + } + }; - // TODO: automatically cleanup the event listener when the frame is deleted - util.addEventListener(window, 'resize', checkSize); + // TODO: automatically cleanup the event listener when the frame is deleted + util.addEventListener(window, 'resize', checkSize); - this.watchTimer = setInterval(checkSize, 1000); + this.watchTimer = setInterval(checkSize, 1000); }; /** @@ -149,12 +149,12 @@ RootPanel.prototype._watch = function () { * @private */ RootPanel.prototype._unwatch = function () { - if (this.watchTimer) { - clearInterval(this.watchTimer); - this.watchTimer = undefined; - } + if (this.watchTimer) { + clearInterval(this.watchTimer); + this.watchTimer = undefined; + } - // TODO: remove event listener on window.resize + // TODO: remove event listener on window.resize }; /** @@ -164,15 +164,15 @@ RootPanel.prototype._unwatch = function () { * as parameter. */ RootPanel.prototype.on = function (event, callback) { - // register the listener at this component - var arr = this.listeners[event]; - if (!arr) { - arr = []; - this.listeners[event] = arr; - } - arr.push(callback); - - this._updateEventEmitters(); + // register the listener at this component + var arr = this.listeners[event]; + if (!arr) { + arr = []; + this.listeners[event] = arr; + } + arr.push(callback); + + this._updateEventEmitters(); }; /** @@ -180,36 +180,36 @@ RootPanel.prototype.on = function (event, callback) { * @private */ RootPanel.prototype._updateEventEmitters = function () { - if (this.listeners) { - var me = this; - util.forEach(this.listeners, function (listeners, event) { - if (!me.emitters) { - me.emitters = {}; - } - if (!(event in me.emitters)) { - // create event - var frame = me.frame; - if (frame) { - //console.log('Created a listener for event ' + event + ' on component ' + me.id); // TODO: cleanup logging - var callback = function(event) { - listeners.forEach(function (listener) { - // TODO: filter on event target! - listener(event); - }); - }; - me.emitters[event] = callback; - - if (!me.hammer) { - me.hammer = Hammer(frame, { - prevent_default: true - }); - } - me.hammer.on(event, callback); - } - } - }); - - // TODO: be able to delete event listeners - // TODO: be able to move event listeners to a parent when available - } + if (this.listeners) { + var me = this; + util.forEach(this.listeners, function (listeners, event) { + if (!me.emitters) { + me.emitters = {}; + } + if (!(event in me.emitters)) { + // create event + var frame = me.frame; + if (frame) { + //console.log('Created a listener for event ' + event + ' on component ' + me.id); // TODO: cleanup logging + var callback = function(event) { + listeners.forEach(function (listener) { + // TODO: filter on event target! + listener(event); + }); + }; + me.emitters[event] = callback; + + if (!me.hammer) { + me.hammer = Hammer(frame, { + prevent_default: true + }); + } + me.hammer.on(event, callback); + } + } + }); + + // TODO: be able to delete event listeners + // TODO: be able to move event listeners to a parent when available + } }; diff --git a/src/timeline/component/TimeAxis.js b/src/timeline/component/TimeAxis.js index 090bd62c..dadb37d2 100644 --- a/src/timeline/component/TimeAxis.js +++ b/src/timeline/component/TimeAxis.js @@ -9,41 +9,41 @@ * @extends Component */ function TimeAxis (parent, depends, options) { - this.id = util.randomUUID(); - this.parent = parent; - this.depends = depends; - - this.dom = { - majorLines: [], - majorTexts: [], - minorLines: [], - minorTexts: [], - redundant: { - majorLines: [], - majorTexts: [], - minorLines: [], - minorTexts: [] - } - }; - this.props = { - range: { - start: 0, - end: 0, - minimumStep: 0 - }, - lineTop: 0 - }; - - this.options = options || {}; - this.defaultOptions = { - orientation: 'bottom', // supported: 'top', 'bottom' - // TODO: implement timeaxis orientations 'left' and 'right' - showMinorLabels: true, - showMajorLabels: true - }; - - this.conversion = null; - this.range = null; + this.id = util.randomUUID(); + this.parent = parent; + this.depends = depends; + + this.dom = { + majorLines: [], + majorTexts: [], + minorLines: [], + minorTexts: [], + redundant: { + majorLines: [], + majorTexts: [], + minorLines: [], + minorTexts: [] + } + }; + this.props = { + range: { + start: 0, + end: 0, + minimumStep: 0 + }, + lineTop: 0 + }; + + this.options = options || {}; + this.defaultOptions = { + orientation: 'bottom', // supported: 'top', 'bottom' + // TODO: implement timeaxis orientations 'left' and 'right' + showMinorLabels: true, + showMajorLabels: true + }; + + this.conversion = null; + this.range = null; } TimeAxis.prototype = new Component(); @@ -56,11 +56,11 @@ TimeAxis.prototype.setOptions = Component.prototype.setOptions; * @param {Range | Object} range A Range or an object containing start and end. */ TimeAxis.prototype.setRange = function (range) { - if (!(range instanceof Range) && (!range || !range.start || !range.end)) { - throw new TypeError('Range must be an instance of Range, ' + - 'or an object containing start and end.'); - } - this.range = range; + if (!(range instanceof Range) && (!range || !range.start || !range.end)) { + throw new TypeError('Range must be an instance of Range, ' + + 'or an object containing start and end.'); + } + this.range = range; }; /** @@ -69,8 +69,8 @@ TimeAxis.prototype.setRange = function (range) { * @return {Date} time The datetime the corresponds with given position x */ TimeAxis.prototype.toTime = function(x) { - var conversion = this.conversion; - return new Date(x / conversion.scale + conversion.offset); + var conversion = this.conversion; + return new Date(x / conversion.scale + conversion.offset); }; /** @@ -81,8 +81,8 @@ TimeAxis.prototype.toTime = function(x) { * @private */ TimeAxis.prototype.toScreen = function(time) { - var conversion = this.conversion; - return (time.valueOf() - conversion.offset) * conversion.scale; + var conversion = this.conversion; + return (time.valueOf() - conversion.offset) * conversion.scale; }; /** @@ -90,112 +90,112 @@ TimeAxis.prototype.toScreen = function(time) { * @return {Boolean} changed */ TimeAxis.prototype.repaint = function () { - var changed = 0, - update = util.updateProperty, - asSize = util.option.asSize, - options = this.options, - orientation = this.getOption('orientation'), - props = this.props, - step = this.step; - - var frame = this.frame; - if (!frame) { - frame = document.createElement('div'); - this.frame = frame; - changed += 1; + var changed = 0, + update = util.updateProperty, + asSize = util.option.asSize, + options = this.options, + orientation = this.getOption('orientation'), + props = this.props, + step = this.step; + + var frame = this.frame; + if (!frame) { + frame = document.createElement('div'); + this.frame = frame; + changed += 1; + } + frame.className = 'axis'; + // TODO: custom className? + + if (!frame.parentNode) { + if (!this.parent) { + throw new Error('Cannot repaint time axis: no parent attached'); } - frame.className = 'axis'; - // TODO: custom className? - - if (!frame.parentNode) { - if (!this.parent) { - throw new Error('Cannot repaint time axis: no parent attached'); - } - var parentContainer = this.parent.getContainer(); - if (!parentContainer) { - throw new Error('Cannot repaint time axis: parent has no container element'); - } - parentContainer.appendChild(frame); - - changed += 1; + var parentContainer = this.parent.getContainer(); + if (!parentContainer) { + throw new Error('Cannot repaint time axis: parent has no container element'); } + parentContainer.appendChild(frame); + + changed += 1; + } + + var parent = frame.parentNode; + if (parent) { + var beforeChild = frame.nextSibling; + parent.removeChild(frame); // take frame offline while updating (is almost twice as fast) + + var defaultTop = (orientation == 'bottom' && this.props.parentHeight && this.height) ? + (this.props.parentHeight - this.height) + 'px' : + '0px'; + changed += update(frame.style, 'top', asSize(options.top, defaultTop)); + changed += update(frame.style, 'left', asSize(options.left, '0px')); + changed += update(frame.style, 'width', asSize(options.width, '100%')); + changed += update(frame.style, 'height', asSize(options.height, this.height + 'px')); + + // get characters width and height + this._repaintMeasureChars(); + + if (this.step) { + this._repaintStart(); + + step.first(); + var xFirstMajorLabel = undefined; + var max = 0; + while (step.hasNext() && max < 1000) { + max++; + var cur = step.getCurrent(), + x = this.toScreen(cur), + isMajor = step.isMajor(); + + // TODO: lines must have a width, such that we can create css backgrounds + + if (this.getOption('showMinorLabels')) { + this._repaintMinorText(x, step.getLabelMinor()); + } - var parent = frame.parentNode; - if (parent) { - var beforeChild = frame.nextSibling; - parent.removeChild(frame); // take frame offline while updating (is almost twice as fast) - - var defaultTop = (orientation == 'bottom' && this.props.parentHeight && this.height) ? - (this.props.parentHeight - this.height) + 'px' : - '0px'; - changed += update(frame.style, 'top', asSize(options.top, defaultTop)); - changed += update(frame.style, 'left', asSize(options.left, '0px')); - changed += update(frame.style, 'width', asSize(options.width, '100%')); - changed += update(frame.style, 'height', asSize(options.height, this.height + 'px')); - - // get characters width and height - this._repaintMeasureChars(); - - if (this.step) { - this._repaintStart(); - - step.first(); - var xFirstMajorLabel = undefined; - var max = 0; - while (step.hasNext() && max < 1000) { - max++; - var cur = step.getCurrent(), - x = this.toScreen(cur), - isMajor = step.isMajor(); - - // TODO: lines must have a width, such that we can create css backgrounds - - if (this.getOption('showMinorLabels')) { - this._repaintMinorText(x, step.getLabelMinor()); - } - - if (isMajor && this.getOption('showMajorLabels')) { - if (x > 0) { - if (xFirstMajorLabel == undefined) { - xFirstMajorLabel = x; - } - this._repaintMajorText(x, step.getLabelMajor()); - } - this._repaintMajorLine(x); - } - else { - this._repaintMinorLine(x); - } - - step.next(); + if (isMajor && this.getOption('showMajorLabels')) { + if (x > 0) { + if (xFirstMajorLabel == undefined) { + xFirstMajorLabel = x; } + this._repaintMajorText(x, step.getLabelMajor()); + } + this._repaintMajorLine(x); + } + else { + this._repaintMinorLine(x); + } - // create a major label on the left when needed - if (this.getOption('showMajorLabels')) { - var leftTime = this.toTime(0), - leftText = step.getLabelMajor(leftTime), - widthText = leftText.length * (props.majorCharWidth || 10) + 10; // upper bound estimation + step.next(); + } - if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) { - this._repaintMajorText(0, leftText); - } - } + // create a major label on the left when needed + if (this.getOption('showMajorLabels')) { + var leftTime = this.toTime(0), + leftText = step.getLabelMajor(leftTime), + widthText = leftText.length * (props.majorCharWidth || 10) + 10; // upper bound estimation - this._repaintEnd(); + if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) { + this._repaintMajorText(0, leftText); } + } - this._repaintLine(); + this._repaintEnd(); + } - // put frame online again - if (beforeChild) { - parent.insertBefore(frame, beforeChild); - } - else { - parent.appendChild(frame) - } + this._repaintLine(); + + // put frame online again + if (beforeChild) { + parent.insertBefore(frame, beforeChild); } + else { + parent.appendChild(frame) + } + } - return (changed > 0); + return (changed > 0); }; /** @@ -204,18 +204,18 @@ TimeAxis.prototype.repaint = function () { * @private */ TimeAxis.prototype._repaintStart = function () { - var dom = this.dom, - redundant = dom.redundant; - - redundant.majorLines = dom.majorLines; - redundant.majorTexts = dom.majorTexts; - redundant.minorLines = dom.minorLines; - redundant.minorTexts = dom.minorTexts; - - dom.majorLines = []; - dom.majorTexts = []; - dom.minorLines = []; - dom.minorTexts = []; + var dom = this.dom, + redundant = dom.redundant; + + redundant.majorLines = dom.majorLines; + redundant.majorTexts = dom.majorTexts; + redundant.minorLines = dom.minorLines; + redundant.minorTexts = dom.minorTexts; + + dom.majorLines = []; + dom.majorTexts = []; + dom.minorLines = []; + dom.minorTexts = []; }; /** @@ -223,14 +223,14 @@ TimeAxis.prototype._repaintStart = function () { * @private */ TimeAxis.prototype._repaintEnd = function () { - util.forEach(this.dom.redundant, function (arr) { - while (arr.length) { - var elem = arr.pop(); - if (elem && elem.parentNode) { - elem.parentNode.removeChild(elem); - } - } - }); + util.forEach(this.dom.redundant, function (arr) { + while (arr.length) { + var elem = arr.pop(); + if (elem && elem.parentNode) { + elem.parentNode.removeChild(elem); + } + } + }); }; @@ -241,23 +241,23 @@ TimeAxis.prototype._repaintEnd = function () { * @private */ TimeAxis.prototype._repaintMinorText = function (x, text) { - // reuse redundant label - var label = this.dom.redundant.minorTexts.shift(); - - if (!label) { - // create new label - var content = document.createTextNode(''); - label = document.createElement('div'); - label.appendChild(content); - label.className = 'text minor'; - this.frame.appendChild(label); - } - this.dom.minorTexts.push(label); - - label.childNodes[0].nodeValue = text; - label.style.left = x + 'px'; - label.style.top = this.props.minorLabelTop + 'px'; - //label.title = title; // TODO: this is a heavy operation + // reuse redundant label + var label = this.dom.redundant.minorTexts.shift(); + + if (!label) { + // create new label + var content = document.createTextNode(''); + label = document.createElement('div'); + label.appendChild(content); + label.className = 'text minor'; + this.frame.appendChild(label); + } + this.dom.minorTexts.push(label); + + label.childNodes[0].nodeValue = text; + label.style.left = x + 'px'; + label.style.top = this.props.minorLabelTop + 'px'; + //label.title = title; // TODO: this is a heavy operation }; /** @@ -267,23 +267,23 @@ TimeAxis.prototype._repaintMinorText = function (x, text) { * @private */ TimeAxis.prototype._repaintMajorText = function (x, text) { - // reuse redundant label - var label = this.dom.redundant.majorTexts.shift(); - - if (!label) { - // create label - var content = document.createTextNode(text); - label = document.createElement('div'); - label.className = 'text major'; - label.appendChild(content); - this.frame.appendChild(label); - } - this.dom.majorTexts.push(label); - - label.childNodes[0].nodeValue = text; - label.style.top = this.props.majorLabelTop + 'px'; - label.style.left = x + 'px'; - //label.title = title; // TODO: this is a heavy operation + // reuse redundant label + var label = this.dom.redundant.majorTexts.shift(); + + if (!label) { + // create label + var content = document.createTextNode(text); + label = document.createElement('div'); + label.className = 'text major'; + label.appendChild(content); + this.frame.appendChild(label); + } + this.dom.majorTexts.push(label); + + label.childNodes[0].nodeValue = text; + label.style.top = this.props.majorLabelTop + 'px'; + label.style.left = x + 'px'; + //label.title = title; // TODO: this is a heavy operation }; /** @@ -292,21 +292,21 @@ TimeAxis.prototype._repaintMajorText = function (x, text) { * @private */ TimeAxis.prototype._repaintMinorLine = function (x) { - // reuse redundant line - var line = this.dom.redundant.minorLines.shift(); - - if (!line) { - // create vertical line - line = document.createElement('div'); - line.className = 'grid vertical minor'; - this.frame.appendChild(line); - } - this.dom.minorLines.push(line); - - var props = this.props; - line.style.top = props.minorLineTop + 'px'; - line.style.height = props.minorLineHeight + 'px'; - line.style.left = (x - props.minorLineWidth / 2) + 'px'; + // reuse redundant line + var line = this.dom.redundant.minorLines.shift(); + + if (!line) { + // create vertical line + line = document.createElement('div'); + line.className = 'grid vertical minor'; + this.frame.appendChild(line); + } + this.dom.minorLines.push(line); + + var props = this.props; + line.style.top = props.minorLineTop + 'px'; + line.style.height = props.minorLineHeight + 'px'; + line.style.left = (x - props.minorLineWidth / 2) + 'px'; }; /** @@ -315,21 +315,21 @@ TimeAxis.prototype._repaintMinorLine = function (x) { * @private */ TimeAxis.prototype._repaintMajorLine = function (x) { - // reuse redundant line - var line = this.dom.redundant.majorLines.shift(); - - if (!line) { - // create vertical line - line = document.createElement('DIV'); - line.className = 'grid vertical major'; - this.frame.appendChild(line); - } - this.dom.majorLines.push(line); - - var props = this.props; - line.style.top = props.majorLineTop + 'px'; - line.style.left = (x - props.majorLineWidth / 2) + 'px'; - line.style.height = props.majorLineHeight + 'px'; + // reuse redundant line + var line = this.dom.redundant.majorLines.shift(); + + if (!line) { + // create vertical line + line = document.createElement('DIV'); + line.className = 'grid vertical major'; + this.frame.appendChild(line); + } + this.dom.majorLines.push(line); + + var props = this.props; + line.style.top = props.majorLineTop + 'px'; + line.style.left = (x - props.majorLineWidth / 2) + 'px'; + line.style.height = props.majorLineHeight + 'px'; }; @@ -338,33 +338,33 @@ TimeAxis.prototype._repaintMajorLine = function (x) { * @private */ TimeAxis.prototype._repaintLine = function() { - var line = this.dom.line, - frame = this.frame, - options = this.options; - - // line before all axis elements - if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) { - if (line) { - // put this line at the end of all childs - frame.removeChild(line); - frame.appendChild(line); - } - else { - // create the axis line - line = document.createElement('div'); - line.className = 'grid horizontal major'; - frame.appendChild(line); - this.dom.line = line; - } - - line.style.top = this.props.lineTop + 'px'; + var line = this.dom.line, + frame = this.frame, + options = this.options; + + // line before all axis elements + if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) { + if (line) { + // put this line at the end of all childs + frame.removeChild(line); + frame.appendChild(line); } else { - if (line && line.parentElement) { - frame.removeChild(line.line); - delete this.dom.line; - } + // create the axis line + line = document.createElement('div'); + line.className = 'grid horizontal major'; + frame.appendChild(line); + this.dom.line = line; + } + + line.style.top = this.props.lineTop + 'px'; + } + else { + if (line && line.parentElement) { + frame.removeChild(line.line); + delete this.dom.line; } + } }; /** @@ -372,31 +372,31 @@ TimeAxis.prototype._repaintLine = function() { * @private */ TimeAxis.prototype._repaintMeasureChars = function () { - // calculate the width and height of a single character - // this is used to calculate the step size, and also the positioning of the - // axis - var dom = this.dom, - text; - - if (!dom.measureCharMinor) { - text = document.createTextNode('0'); - var measureCharMinor = document.createElement('DIV'); - measureCharMinor.className = 'text minor measure'; - measureCharMinor.appendChild(text); - this.frame.appendChild(measureCharMinor); - - dom.measureCharMinor = measureCharMinor; - } - - if (!dom.measureCharMajor) { - text = document.createTextNode('0'); - var measureCharMajor = document.createElement('DIV'); - measureCharMajor.className = 'text major measure'; - measureCharMajor.appendChild(text); - this.frame.appendChild(measureCharMajor); - - dom.measureCharMajor = measureCharMajor; - } + // calculate the width and height of a single character + // this is used to calculate the step size, and also the positioning of the + // axis + var dom = this.dom, + text; + + if (!dom.measureCharMinor) { + text = document.createTextNode('0'); + var measureCharMinor = document.createElement('DIV'); + measureCharMinor.className = 'text minor measure'; + measureCharMinor.appendChild(text); + this.frame.appendChild(measureCharMinor); + + dom.measureCharMinor = measureCharMinor; + } + + if (!dom.measureCharMajor) { + text = document.createTextNode('0'); + var measureCharMajor = document.createElement('DIV'); + measureCharMajor.className = 'text major measure'; + measureCharMajor.appendChild(text); + this.frame.appendChild(measureCharMajor); + + dom.measureCharMajor = measureCharMajor; + } }; /** @@ -404,100 +404,100 @@ TimeAxis.prototype._repaintMeasureChars = function () { * @return {Boolean} resized */ TimeAxis.prototype.reflow = function () { - var changed = 0, - update = util.updateProperty, - frame = this.frame, - range = this.range; + var changed = 0, + update = util.updateProperty, + frame = this.frame, + range = this.range; + + if (!range) { + throw new Error('Cannot repaint time axis: no range configured'); + } + + if (frame) { + changed += update(this, 'top', frame.offsetTop); + changed += update(this, 'left', frame.offsetLeft); + + // calculate size of a character + var props = this.props, + showMinorLabels = this.getOption('showMinorLabels'), + showMajorLabels = this.getOption('showMajorLabels'), + measureCharMinor = this.dom.measureCharMinor, + measureCharMajor = this.dom.measureCharMajor; + if (measureCharMinor) { + props.minorCharHeight = measureCharMinor.clientHeight; + props.minorCharWidth = measureCharMinor.clientWidth; + } + if (measureCharMajor) { + props.majorCharHeight = measureCharMajor.clientHeight; + props.majorCharWidth = measureCharMajor.clientWidth; + } - if (!range) { - throw new Error('Cannot repaint time axis: no range configured'); + var parentHeight = frame.parentNode ? frame.parentNode.offsetHeight : 0; + if (parentHeight != props.parentHeight) { + props.parentHeight = parentHeight; + changed += 1; } + switch (this.getOption('orientation')) { + case 'bottom': + props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; + props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0; - if (frame) { - changed += update(this, 'top', frame.offsetTop); - changed += update(this, 'left', frame.offsetLeft); - - // calculate size of a character - var props = this.props, - showMinorLabels = this.getOption('showMinorLabels'), - showMajorLabels = this.getOption('showMajorLabels'), - measureCharMinor = this.dom.measureCharMinor, - measureCharMajor = this.dom.measureCharMajor; - if (measureCharMinor) { - props.minorCharHeight = measureCharMinor.clientHeight; - props.minorCharWidth = measureCharMinor.clientWidth; - } - if (measureCharMajor) { - props.majorCharHeight = measureCharMajor.clientHeight; - props.majorCharWidth = measureCharMajor.clientWidth; - } + props.minorLabelTop = 0; + props.majorLabelTop = props.minorLabelTop + props.minorLabelHeight; - var parentHeight = frame.parentNode ? frame.parentNode.offsetHeight : 0; - if (parentHeight != props.parentHeight) { - props.parentHeight = parentHeight; - changed += 1; - } - switch (this.getOption('orientation')) { - case 'bottom': - props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; - props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0; + props.minorLineTop = -this.top; + props.minorLineHeight = Math.max(this.top + props.majorLabelHeight, 0); + props.minorLineWidth = 1; // TODO: really calculate width - props.minorLabelTop = 0; - props.majorLabelTop = props.minorLabelTop + props.minorLabelHeight; + props.majorLineTop = -this.top; + props.majorLineHeight = Math.max(this.top + props.minorLabelHeight + props.majorLabelHeight, 0); + props.majorLineWidth = 1; // TODO: really calculate width - props.minorLineTop = -this.top; - props.minorLineHeight = Math.max(this.top + props.majorLabelHeight, 0); - props.minorLineWidth = 1; // TODO: really calculate width + props.lineTop = 0; - props.majorLineTop = -this.top; - props.majorLineHeight = Math.max(this.top + props.minorLabelHeight + props.majorLabelHeight, 0); - props.majorLineWidth = 1; // TODO: really calculate width + break; - props.lineTop = 0; + case 'top': + props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; + props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0; - break; + props.majorLabelTop = 0; + props.minorLabelTop = props.majorLabelTop + props.majorLabelHeight; - case 'top': - props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; - props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0; + props.minorLineTop = props.minorLabelTop; + props.minorLineHeight = Math.max(parentHeight - props.majorLabelHeight - this.top); + props.minorLineWidth = 1; // TODO: really calculate width - props.majorLabelTop = 0; - props.minorLabelTop = props.majorLabelTop + props.majorLabelHeight; + props.majorLineTop = 0; + props.majorLineHeight = Math.max(parentHeight - this.top); + props.majorLineWidth = 1; // TODO: really calculate width - props.minorLineTop = props.minorLabelTop; - props.minorLineHeight = Math.max(parentHeight - props.majorLabelHeight - this.top); - props.minorLineWidth = 1; // TODO: really calculate width + props.lineTop = props.majorLabelHeight + props.minorLabelHeight; - props.majorLineTop = 0; - props.majorLineHeight = Math.max(parentHeight - this.top); - props.majorLineWidth = 1; // TODO: really calculate width + break; - props.lineTop = props.majorLabelHeight + props.minorLabelHeight; + default: + throw new Error('Unkown orientation "' + this.getOption('orientation') + '"'); + } - break; + var height = props.minorLabelHeight + props.majorLabelHeight; + changed += update(this, 'width', frame.offsetWidth); + changed += update(this, 'height', height); - default: - throw new Error('Unkown orientation "' + this.getOption('orientation') + '"'); - } + // calculate range and step + this._updateConversion(); - var height = props.minorLabelHeight + props.majorLabelHeight; - changed += update(this, 'width', frame.offsetWidth); - changed += update(this, 'height', height); - - // calculate range and step - this._updateConversion(); - - var start = util.convert(range.start, 'Number'), - end = util.convert(range.end, 'Number'), - minimumStep = this.toTime((props.minorCharWidth || 10) * 5).valueOf() - -this.toTime(0).valueOf(); - this.step = new TimeStep(new Date(start), new Date(end), minimumStep); - changed += update(props.range, 'start', start); - changed += update(props.range, 'end', end); - changed += update(props.range, 'minimumStep', minimumStep.valueOf()); - } + var start = util.convert(range.start, 'Number'), + end = util.convert(range.end, 'Number'), + minimumStep = this.toTime((props.minorCharWidth || 10) * 5).valueOf() + -this.toTime(0).valueOf(); + this.step = new TimeStep(new Date(start), new Date(end), minimumStep); + changed += update(props.range, 'start', start); + changed += update(props.range, 'end', end); + changed += update(props.range, 'minimumStep', minimumStep.valueOf()); + } - return (changed > 0); + return (changed > 0); }; /** @@ -508,15 +508,15 @@ TimeAxis.prototype.reflow = function () { * @private */ TimeAxis.prototype._updateConversion = function() { - var range = this.range; - if (!range) { - throw new Error('No range configured'); - } - - if (range.conversion) { - this.conversion = range.conversion(this.width); - } - else { - this.conversion = Range.conversion(range.start, range.end, this.width); - } + var range = this.range; + if (!range) { + throw new Error('No range configured'); + } + + if (range.conversion) { + this.conversion = range.conversion(this.width); + } + else { + this.conversion = Range.conversion(range.start, range.end, this.width); + } }; diff --git a/src/timeline/component/css/currenttime.css b/src/timeline/component/css/currenttime.css index 5ea0380b..73438693 100644 --- a/src/timeline/component/css/currenttime.css +++ b/src/timeline/component/css/currenttime.css @@ -1,5 +1,5 @@ .vis.timeline .currenttime { - background-color: #FF7F6E; - width: 2px; - z-index: 9; + background-color: #FF7F6E; + width: 2px; + z-index: 9; } \ No newline at end of file diff --git a/src/timeline/component/css/customtime.css b/src/timeline/component/css/customtime.css index 15a3792a..76ce38fe 100644 --- a/src/timeline/component/css/customtime.css +++ b/src/timeline/component/css/customtime.css @@ -1,6 +1,6 @@ .vis.timeline .customtime { - background-color: #6E94FF; - width: 2px; - cursor: move; - z-index: 9; + background-color: #6E94FF; + width: 2px; + cursor: move; + z-index: 9; } \ No newline at end of file diff --git a/src/timeline/component/css/groupset.css b/src/timeline/component/css/groupset.css index 590c3b82..b6467a7f 100644 --- a/src/timeline/component/css/groupset.css +++ b/src/timeline/component/css/groupset.css @@ -1,59 +1,59 @@ .vis.timeline .groupset { - position: absolute; - padding: 0; - margin: 0; + position: absolute; + padding: 0; + margin: 0; } .vis.timeline .labels { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - - padding: 0; - margin: 0; - - border-right: 1px solid #bfbfbf; - box-sizing: border-box; - -moz-box-sizing: border-box; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + padding: 0; + margin: 0; + + border-right: 1px solid #bfbfbf; + box-sizing: border-box; + -moz-box-sizing: border-box; } .vis.timeline .labels .label-set { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; - overflow: hidden; + overflow: hidden; - border-top: none; - border-bottom: 1px solid #bfbfbf; + border-top: none; + border-bottom: 1px solid #bfbfbf; } .vis.timeline .labels .label-set .label { - position: absolute; - left: 0; - top: 0; - width: 100%; - color: #4d4d4d; + position: absolute; + left: 0; + top: 0; + width: 100%; + color: #4d4d4d; } .vis.timeline.top .labels .label-set .label, .vis.timeline.top .groupset .itemset-axis { -border-top: 1px solid #bfbfbf; - border-bottom: none; + border-top: 1px solid #bfbfbf; + border-bottom: none; } .vis.timeline.bottom .labels .label-set .label, .vis.timeline.bottom .groupset .itemset-axis { - border-top: none; - border-bottom: 1px solid #bfbfbf; + border-top: none; + border-bottom: 1px solid #bfbfbf; } .vis.timeline .labels .label-set .label .inner { - display: inline-block; - padding: 5px; + display: inline-block; + padding: 5px; } diff --git a/src/timeline/component/css/item.css b/src/timeline/component/css/item.css index 13937964..579d1e5e 100644 --- a/src/timeline/component/css/item.css +++ b/src/timeline/component/css/item.css @@ -1,85 +1,85 @@ .vis.timeline .item { - position: absolute; - color: #1A1A1A; - border-color: #97B0F8; - background-color: #D5DDF6; - display: inline-block; + position: absolute; + color: #1A1A1A; + border-color: #97B0F8; + background-color: #D5DDF6; + display: inline-block; } .vis.timeline .item.selected { - border-color: #FFC200; - background-color: #FFF785; - z-index: 999; + border-color: #FFC200; + background-color: #FFF785; + z-index: 999; } .vis.timeline .item.cluster { - /* TODO: use another color or pattern? */ - background: #97B0F8 url('img/cluster_bg.png'); - color: white; + /* TODO: use another color or pattern? */ + background: #97B0F8 url('img/cluster_bg.png'); + color: white; } .vis.timeline .item.cluster.point { - border-color: #D5DDF6; + border-color: #D5DDF6; } .vis.timeline .item.box { - text-align: center; - border-style: solid; - border-width: 1px; - border-radius: 5px; - -moz-border-radius: 5px; /* For Firefox 3.6 and older */ + text-align: center; + border-style: solid; + border-width: 1px; + border-radius: 5px; + -moz-border-radius: 5px; /* For Firefox 3.6 and older */ } .vis.timeline .item.point { - background: none; + background: none; } .vis.timeline .dot { - border: 5px solid #97B0F8; - position: absolute; - border-radius: 5px; - -moz-border-radius: 5px; /* For Firefox 3.6 and older */ + border: 5px solid #97B0F8; + position: absolute; + border-radius: 5px; + -moz-border-radius: 5px; /* For Firefox 3.6 and older */ } .vis.timeline .item.range { - overflow: hidden; - border-style: solid; - border-width: 1px; - border-radius: 2px; - -moz-border-radius: 2px; /* For Firefox 3.6 and older */ + overflow: hidden; + border-style: solid; + border-width: 1px; + border-radius: 2px; + -moz-border-radius: 2px; /* For Firefox 3.6 and older */ } .vis.timeline .item.rangeoverflow { - border-style: solid; - border-width: 1px; - border-radius: 2px; - -moz-border-radius: 2px; /* For Firefox 3.6 and older */ + border-style: solid; + border-width: 1px; + border-radius: 2px; + -moz-border-radius: 2px; /* For Firefox 3.6 and older */ } .vis.timeline .item.range .drag-left, .vis.timeline .item.rangeoverflow .drag-left { - cursor: w-resize; - z-index: 1000; + cursor: w-resize; + z-index: 1000; } .vis.timeline .item.range .drag-right, .vis.timeline .item.rangeoverflow .drag-right { - cursor: e-resize; - z-index: 1000; + cursor: e-resize; + z-index: 1000; } .vis.timeline .item.range .content, .vis.timeline .item.rangeoverflow .content { - position: relative; - display: inline-block; + position: relative; + display: inline-block; } .vis.timeline .item.line { - position: absolute; - width: 0; - border-left-width: 1px; - border-left-style: solid; + position: absolute; + width: 0; + border-left-width: 1px; + border-left-style: solid; } .vis.timeline .item .content { - margin: 5px; - white-space: nowrap; - overflow: hidden; + margin: 5px; + white-space: nowrap; + overflow: hidden; } diff --git a/src/timeline/component/css/itemset.css b/src/timeline/component/css/itemset.css index 3e1d2885..c21d8fa1 100644 --- a/src/timeline/component/css/itemset.css +++ b/src/timeline/component/css/itemset.css @@ -1,9 +1,9 @@ .vis.timeline .itemset { - position: absolute; - padding: 0; - margin: 0; - overflow: hidden; + position: absolute; + padding: 0; + margin: 0; + overflow: hidden; } .vis.timeline .background { @@ -13,5 +13,5 @@ } .vis.timeline .itemset-axis { - position: absolute; + position: absolute; } diff --git a/src/timeline/component/css/panel.css b/src/timeline/component/css/panel.css index f87bbf3b..819f33f2 100644 --- a/src/timeline/component/css/panel.css +++ b/src/timeline/component/css/panel.css @@ -1,14 +1,14 @@ .vis.timeline.rootpanel { - position: relative; - overflow: hidden; + position: relative; + overflow: hidden; - border: 1px solid #bfbfbf; - -moz-box-sizing: border-box; - box-sizing: border-box; + border: 1px solid #bfbfbf; + -moz-box-sizing: border-box; + box-sizing: border-box; } .vis.timeline .panel { - position: absolute; - overflow: hidden; + position: absolute; + overflow: hidden; } diff --git a/src/timeline/component/css/timeaxis.css b/src/timeline/component/css/timeaxis.css index 0d86b860..91b655b7 100644 --- a/src/timeline/component/css/timeaxis.css +++ b/src/timeline/component/css/timeaxis.css @@ -1,41 +1,41 @@ .vis.timeline .axis { - position: relative; + position: relative; } .vis.timeline .axis .text { - position: absolute; - color: #4d4d4d; - padding: 3px; - white-space: nowrap; + position: absolute; + color: #4d4d4d; + padding: 3px; + white-space: nowrap; } .vis.timeline .axis .text.measure { - position: absolute; - padding-left: 0; - padding-right: 0; - margin-left: 0; - margin-right: 0; - visibility: hidden; + position: absolute; + padding-left: 0; + padding-right: 0; + margin-left: 0; + margin-right: 0; + visibility: hidden; } .vis.timeline .axis .grid.vertical { - position: absolute; - width: 0; - border-right: 1px solid; + position: absolute; + width: 0; + border-right: 1px solid; } .vis.timeline .axis .grid.horizontal { - position: absolute; - left: 0; - width: 100%; - height: 0; - border-bottom: 1px solid; + position: absolute; + left: 0; + width: 100%; + height: 0; + border-bottom: 1px solid; } .vis.timeline .axis .grid.minor { - border-color: #e5e5e5; + border-color: #e5e5e5; } .vis.timeline .axis .grid.major { - border-color: #bfbfbf; + border-color: #bfbfbf; } diff --git a/src/timeline/component/item/Item.js b/src/timeline/component/item/Item.js index 0a5f623e..a706993f 100644 --- a/src/timeline/component/item/Item.js +++ b/src/timeline/component/item/Item.js @@ -8,32 +8,32 @@ * // TODO: describe available options */ function Item (parent, data, options, defaultOptions) { - this.parent = parent; - this.data = data; - this.dom = null; - this.options = options || {}; - this.defaultOptions = defaultOptions || {}; + this.parent = parent; + this.data = data; + this.dom = null; + this.options = options || {}; + this.defaultOptions = defaultOptions || {}; - this.selected = false; - this.visible = false; - this.top = 0; - this.left = 0; - this.width = 0; - this.height = 0; + this.selected = false; + this.visible = false; + this.top = 0; + this.left = 0; + this.width = 0; + this.height = 0; } /** * Select current item */ Item.prototype.select = function select() { - this.selected = true; + this.selected = true; }; /** * Unselect current item */ Item.prototype.unselect = function unselect() { - this.selected = false; + this.selected = false; }; /** @@ -41,7 +41,7 @@ Item.prototype.unselect = function unselect() { * @return {Boolean} changed */ Item.prototype.show = function show() { - return false; + return false; }; /** @@ -49,7 +49,7 @@ Item.prototype.show = function show() { * @return {Boolean} changed */ Item.prototype.hide = function hide() { - return false; + return false; }; /** @@ -57,8 +57,8 @@ Item.prototype.hide = function hide() { * @return {Boolean} changed */ Item.prototype.repaint = function repaint() { - // should be implemented by the item - return false; + // should be implemented by the item + return false; }; /** @@ -66,8 +66,8 @@ Item.prototype.repaint = function repaint() { * @return {Boolean} resized */ Item.prototype.reflow = function reflow() { - // should be implemented by the item - return false; + // should be implemented by the item + return false; }; /** @@ -75,5 +75,5 @@ Item.prototype.reflow = function reflow() { * @return {Integer} width */ Item.prototype.getWidth = function getWidth() { - return this.width; + return this.width; } diff --git a/src/timeline/component/item/ItemBox.js b/src/timeline/component/item/ItemBox.js index 48253cc4..4ad671c6 100644 --- a/src/timeline/component/item/ItemBox.js +++ b/src/timeline/component/item/ItemBox.js @@ -9,22 +9,22 @@ * // TODO: describe available options */ function ItemBox (parent, data, options, defaultOptions) { - this.props = { - dot: { - left: 0, - top: 0, - width: 0, - height: 0 - }, - line: { - top: 0, - left: 0, - width: 0, - height: 0 - } - }; - - Item.call(this, parent, data, options, defaultOptions); + this.props = { + dot: { + left: 0, + top: 0, + width: 0, + height: 0 + }, + line: { + top: 0, + left: 0, + width: 0, + height: 0 + } + }; + + Item.call(this, parent, data, options, defaultOptions); } ItemBox.prototype = new Item (null, null); @@ -34,8 +34,8 @@ ItemBox.prototype = new Item (null, null); * @override */ ItemBox.prototype.select = function select() { - this.selected = true; - // TODO: select and unselect + this.selected = true; + // TODO: select and unselect }; /** @@ -43,8 +43,8 @@ ItemBox.prototype.select = function select() { * @override */ ItemBox.prototype.unselect = function unselect() { - this.selected = false; - // TODO: select and unselect + this.selected = false; + // TODO: select and unselect }; /** @@ -52,80 +52,80 @@ ItemBox.prototype.unselect = function unselect() { * @return {Boolean} changed */ ItemBox.prototype.repaint = function repaint() { - // TODO: make an efficient repaint - var changed = false; - var dom = this.dom; - - if (!dom) { - this._create(); - dom = this.dom; - changed = true; + // TODO: make an efficient repaint + var changed = false; + var dom = this.dom; + + if (!dom) { + this._create(); + dom = this.dom; + changed = true; + } + + if (dom) { + if (!this.parent) { + throw new Error('Cannot repaint item: no parent attached'); } - if (dom) { - if (!this.parent) { - throw new Error('Cannot repaint item: no parent attached'); - } - - if (!dom.box.parentNode) { - var foreground = this.parent.getForeground(); - if (!foreground) { - throw new Error('Cannot repaint time axis: ' + - 'parent has no foreground container element'); - } - foreground.appendChild(dom.box); - changed = true; - } - - if (!dom.line.parentNode) { - var background = this.parent.getBackground(); - if (!background) { - throw new Error('Cannot repaint time axis: ' + - 'parent has no background container element'); - } - background.appendChild(dom.line); - changed = true; - } - - if (!dom.dot.parentNode) { - var axis = this.parent.getAxis(); - if (!background) { - throw new Error('Cannot repaint time axis: ' + - 'parent has no axis container element'); - } - axis.appendChild(dom.dot); - changed = true; - } - - // update contents - if (this.data.content != this.content) { - this.content = this.data.content; - if (this.content instanceof Element) { - dom.content.innerHTML = ''; - dom.content.appendChild(this.content); - } - else if (this.data.content != undefined) { - dom.content.innerHTML = this.content; - } - else { - throw new Error('Property "content" missing in item ' + this.data.id); - } - changed = true; - } - - // update class - var className = (this.data.className? ' ' + this.data.className : '') + - (this.selected ? ' selected' : ''); - if (this.className != className) { - this.className = className; - dom.box.className = 'item box' + className; - dom.line.className = 'item line' + className; - dom.dot.className = 'item dot' + className; - changed = true; - } + if (!dom.box.parentNode) { + var foreground = this.parent.getForeground(); + if (!foreground) { + throw new Error('Cannot repaint time axis: ' + + 'parent has no foreground container element'); + } + foreground.appendChild(dom.box); + changed = true; + } + + if (!dom.line.parentNode) { + var background = this.parent.getBackground(); + if (!background) { + throw new Error('Cannot repaint time axis: ' + + 'parent has no background container element'); + } + background.appendChild(dom.line); + changed = true; + } + + if (!dom.dot.parentNode) { + var axis = this.parent.getAxis(); + if (!background) { + throw new Error('Cannot repaint time axis: ' + + 'parent has no axis container element'); + } + axis.appendChild(dom.dot); + changed = true; } - return changed; + // update contents + if (this.data.content != this.content) { + this.content = this.data.content; + if (this.content instanceof Element) { + dom.content.innerHTML = ''; + dom.content.appendChild(this.content); + } + else if (this.data.content != undefined) { + dom.content.innerHTML = this.content; + } + else { + throw new Error('Property "content" missing in item ' + this.data.id); + } + changed = true; + } + + // update class + var className = (this.data.className? ' ' + this.data.className : '') + + (this.selected ? ' selected' : ''); + if (this.className != className) { + this.className = className; + dom.box.className = 'item box' + className; + dom.line.className = 'item line' + className; + dom.dot.className = 'item dot' + className; + changed = true; + } + } + + return changed; }; /** @@ -134,12 +134,12 @@ ItemBox.prototype.repaint = function repaint() { * @return {Boolean} changed */ ItemBox.prototype.show = function show() { - if (!this.dom || !this.dom.box.parentNode) { - return this.repaint(); - } - else { - return false; - } + if (!this.dom || !this.dom.box.parentNode) { + return this.repaint(); + } + else { + return false; + } }; /** @@ -147,21 +147,21 @@ ItemBox.prototype.show = function show() { * @return {Boolean} changed */ ItemBox.prototype.hide = function hide() { - var changed = false, - dom = this.dom; - if (dom) { - if (dom.box.parentNode) { - dom.box.parentNode.removeChild(dom.box); - changed = true; - } - if (dom.line.parentNode) { - dom.line.parentNode.removeChild(dom.line); - } - if (dom.dot.parentNode) { - dom.dot.parentNode.removeChild(dom.dot); - } + var changed = false, + dom = this.dom; + if (dom) { + if (dom.box.parentNode) { + dom.box.parentNode.removeChild(dom.box); + changed = true; + } + if (dom.line.parentNode) { + dom.line.parentNode.removeChild(dom.line); } - return changed; + if (dom.dot.parentNode) { + dom.dot.parentNode.removeChild(dom.dot); + } + } + return changed; }; /** @@ -170,87 +170,87 @@ ItemBox.prototype.hide = function hide() { * @override */ ItemBox.prototype.reflow = function reflow() { - var changed = 0, - update, - dom, - props, - options, - margin, - start, - align, - orientation, - top, - left, - data, - range; - - if (this.data.start == undefined) { - throw new Error('Property "start" missing in item ' + this.data.id); - } - - data = this.data; - range = this.parent && this.parent.range; - if (data && range) { - // TODO: account for the width of the item - var interval = (range.end - range.start); - this.visible = (data.start > range.start - interval) && (data.start < range.end + interval); + var changed = 0, + update, + dom, + props, + options, + margin, + start, + align, + orientation, + top, + left, + data, + range; + + if (this.data.start == undefined) { + throw new Error('Property "start" missing in item ' + this.data.id); + } + + data = this.data; + range = this.parent && this.parent.range; + if (data && range) { + // TODO: account for the width of the item + var interval = (range.end - range.start); + this.visible = (data.start > range.start - interval) && (data.start < range.end + interval); + } + else { + this.visible = false; + } + + if (this.visible) { + dom = this.dom; + if (dom) { + update = util.updateProperty; + props = this.props; + options = this.options; + start = this.parent.toScreen(this.data.start); + align = options.align || this.defaultOptions.align; + margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis; + orientation = options.orientation || this.defaultOptions.orientation; + + changed += update(props.dot, 'height', dom.dot.offsetHeight); + changed += update(props.dot, 'width', dom.dot.offsetWidth); + changed += update(props.line, 'width', dom.line.offsetWidth); + changed += update(props.line, 'height', dom.line.offsetHeight); + changed += update(props.line, 'top', dom.line.offsetTop); + changed += update(this, 'width', dom.box.offsetWidth); + changed += update(this, 'height', dom.box.offsetHeight); + if (align == 'right') { + left = start - this.width; + } + else if (align == 'left') { + left = start; + } + else { + // default or 'center' + left = start - this.width / 2; + } + changed += update(this, 'left', left); + + changed += update(props.line, 'left', start - props.line.width / 2); + changed += update(props.dot, 'left', start - props.dot.width / 2); + changed += update(props.dot, 'top', -props.dot.height / 2); + if (orientation == 'top') { + top = margin; + + changed += update(this, 'top', top); + } + else { + // default or 'bottom' + var parentHeight = this.parent.height; + top = parentHeight - this.height - margin; + + changed += update(this, 'top', top); + } } else { - this.visible = false; - } - - if (this.visible) { - dom = this.dom; - if (dom) { - update = util.updateProperty; - props = this.props; - options = this.options; - start = this.parent.toScreen(this.data.start); - align = options.align || this.defaultOptions.align; - margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis; - orientation = options.orientation || this.defaultOptions.orientation; - - changed += update(props.dot, 'height', dom.dot.offsetHeight); - changed += update(props.dot, 'width', dom.dot.offsetWidth); - changed += update(props.line, 'width', dom.line.offsetWidth); - changed += update(props.line, 'height', dom.line.offsetHeight); - changed += update(props.line, 'top', dom.line.offsetTop); - changed += update(this, 'width', dom.box.offsetWidth); - changed += update(this, 'height', dom.box.offsetHeight); - if (align == 'right') { - left = start - this.width; - } - else if (align == 'left') { - left = start; - } - else { - // default or 'center' - left = start - this.width / 2; - } - changed += update(this, 'left', left); - - changed += update(props.line, 'left', start - props.line.width / 2); - changed += update(props.dot, 'left', start - props.dot.width / 2); - changed += update(props.dot, 'top', -props.dot.height / 2); - if (orientation == 'top') { - top = margin; - - changed += update(this, 'top', top); - } - else { - // default or 'bottom' - var parentHeight = this.parent.height; - top = parentHeight - this.height - margin; - - changed += update(this, 'top', top); - } - } - else { - changed += 1; - } + changed += 1; } + } - return (changed > 0); + return (changed > 0); }; /** @@ -258,27 +258,27 @@ ItemBox.prototype.reflow = function reflow() { * @private */ ItemBox.prototype._create = function _create() { - var dom = this.dom; - if (!dom) { - this.dom = dom = {}; - - // create the box - dom.box = document.createElement('DIV'); - // className is updated in repaint() - - // contents box (inside the background box). used for making margins - dom.content = document.createElement('DIV'); - dom.content.className = 'content'; - dom.box.appendChild(dom.content); - - // line to axis - dom.line = document.createElement('DIV'); - dom.line.className = 'line'; - - // dot on axis - dom.dot = document.createElement('DIV'); - dom.dot.className = 'dot'; - } + var dom = this.dom; + if (!dom) { + this.dom = dom = {}; + + // create the box + dom.box = document.createElement('DIV'); + // className is updated in repaint() + + // contents box (inside the background box). used for making margins + dom.content = document.createElement('DIV'); + dom.content.className = 'content'; + dom.box.appendChild(dom.content); + + // line to axis + dom.line = document.createElement('DIV'); + dom.line.className = 'line'; + + // dot on axis + dom.dot = document.createElement('DIV'); + dom.dot.className = 'dot'; + } }; /** @@ -287,31 +287,31 @@ ItemBox.prototype._create = function _create() { * @override */ ItemBox.prototype.reposition = function reposition() { - var dom = this.dom, - props = this.props, - orientation = this.options.orientation || this.defaultOptions.orientation; - - if (dom) { - var box = dom.box, - line = dom.line, - dot = dom.dot; - - box.style.left = this.left + 'px'; - box.style.top = this.top + 'px'; - - line.style.left = props.line.left + 'px'; - if (orientation == 'top') { - line.style.top = 0 + 'px'; - line.style.height = this.top + 'px'; - } - else { - // orientation 'bottom' - line.style.top = (this.top + this.height) + 'px'; - line.style.height = Math.max(this.parent.height - this.top - this.height + - this.props.dot.height / 2, 0) + 'px'; - } - - dot.style.left = props.dot.left + 'px'; - dot.style.top = props.dot.top + 'px'; + var dom = this.dom, + props = this.props, + orientation = this.options.orientation || this.defaultOptions.orientation; + + if (dom) { + var box = dom.box, + line = dom.line, + dot = dom.dot; + + box.style.left = this.left + 'px'; + box.style.top = this.top + 'px'; + + line.style.left = props.line.left + 'px'; + if (orientation == 'top') { + line.style.top = 0 + 'px'; + line.style.height = this.top + 'px'; } + else { + // orientation 'bottom' + line.style.top = (this.top + this.height) + 'px'; + line.style.height = Math.max(this.parent.height - this.top - this.height + + this.props.dot.height / 2, 0) + 'px'; + } + + dot.style.left = props.dot.left + 'px'; + dot.style.top = props.dot.top + 'px'; + } }; diff --git a/src/timeline/component/item/ItemPoint.js b/src/timeline/component/item/ItemPoint.js index ec5b7d84..1d0d7ce7 100644 --- a/src/timeline/component/item/ItemPoint.js +++ b/src/timeline/component/item/ItemPoint.js @@ -9,19 +9,19 @@ * // TODO: describe available options */ function ItemPoint (parent, data, options, defaultOptions) { - this.props = { - dot: { - top: 0, - width: 0, - height: 0 - }, - content: { - height: 0, - marginLeft: 0 - } - }; - - Item.call(this, parent, data, options, defaultOptions); + this.props = { + dot: { + top: 0, + width: 0, + height: 0 + }, + content: { + height: 0, + marginLeft: 0 + } + }; + + Item.call(this, parent, data, options, defaultOptions); } ItemPoint.prototype = new Item (null, null); @@ -31,8 +31,8 @@ ItemPoint.prototype = new Item (null, null); * @override */ ItemPoint.prototype.select = function select() { - this.selected = true; - // TODO: select and unselect + this.selected = true; + // TODO: select and unselect }; /** @@ -40,8 +40,8 @@ ItemPoint.prototype.select = function select() { * @override */ ItemPoint.prototype.unselect = function unselect() { - this.selected = false; - // TODO: select and unselect + this.selected = false; + // TODO: select and unselect }; /** @@ -49,59 +49,59 @@ ItemPoint.prototype.unselect = function unselect() { * @return {Boolean} changed */ ItemPoint.prototype.repaint = function repaint() { - // TODO: make an efficient repaint - var changed = false; - var dom = this.dom; - - if (!dom) { - this._create(); - dom = this.dom; - changed = true; + // TODO: make an efficient repaint + var changed = false; + var dom = this.dom; + + if (!dom) { + this._create(); + dom = this.dom; + changed = true; + } + + if (dom) { + if (!this.parent) { + throw new Error('Cannot repaint item: no parent attached'); + } + var foreground = this.parent.getForeground(); + if (!foreground) { + throw new Error('Cannot repaint time axis: ' + + 'parent has no foreground container element'); } - if (dom) { - if (!this.parent) { - throw new Error('Cannot repaint item: no parent attached'); - } - var foreground = this.parent.getForeground(); - if (!foreground) { - throw new Error('Cannot repaint time axis: ' + - 'parent has no foreground container element'); - } - - if (!dom.point.parentNode) { - foreground.appendChild(dom.point); - foreground.appendChild(dom.point); - changed = true; - } - - // update contents - if (this.data.content != this.content) { - this.content = this.data.content; - if (this.content instanceof Element) { - dom.content.innerHTML = ''; - dom.content.appendChild(this.content); - } - else if (this.data.content != undefined) { - dom.content.innerHTML = this.content; - } - else { - throw new Error('Property "content" missing in item ' + this.data.id); - } - changed = true; - } - - // update class - var className = (this.data.className? ' ' + this.data.className : '') + - (this.selected ? ' selected' : ''); - if (this.className != className) { - this.className = className; - dom.point.className = 'item point' + className; - changed = true; - } + if (!dom.point.parentNode) { + foreground.appendChild(dom.point); + foreground.appendChild(dom.point); + changed = true; + } + + // update contents + if (this.data.content != this.content) { + this.content = this.data.content; + if (this.content instanceof Element) { + dom.content.innerHTML = ''; + dom.content.appendChild(this.content); + } + else if (this.data.content != undefined) { + dom.content.innerHTML = this.content; + } + else { + throw new Error('Property "content" missing in item ' + this.data.id); + } + changed = true; + } + + // update class + var className = (this.data.className? ' ' + this.data.className : '') + + (this.selected ? ' selected' : ''); + if (this.className != className) { + this.className = className; + dom.point.className = 'item point' + className; + changed = true; } + } - return changed; + return changed; }; /** @@ -110,12 +110,12 @@ ItemPoint.prototype.repaint = function repaint() { * @return {Boolean} changed */ ItemPoint.prototype.show = function show() { - if (!this.dom || !this.dom.point.parentNode) { - return this.repaint(); - } - else { - return false; - } + if (!this.dom || !this.dom.point.parentNode) { + return this.repaint(); + } + else { + return false; + } }; /** @@ -123,15 +123,15 @@ ItemPoint.prototype.show = function show() { * @return {Boolean} changed */ ItemPoint.prototype.hide = function hide() { - var changed = false, - dom = this.dom; - if (dom) { - if (dom.point.parentNode) { - dom.point.parentNode.removeChild(dom.point); - changed = true; - } + var changed = false, + dom = this.dom; + if (dom) { + if (dom.point.parentNode) { + dom.point.parentNode.removeChild(dom.point); + changed = true; } - return changed; + } + return changed; }; /** @@ -140,70 +140,70 @@ ItemPoint.prototype.hide = function hide() { * @override */ ItemPoint.prototype.reflow = function reflow() { - var changed = 0, - update, - dom, - props, - options, - margin, - orientation, - start, - top, - data, - range; - - if (this.data.start == undefined) { - throw new Error('Property "start" missing in item ' + this.data.id); - } - - data = this.data; - range = this.parent && this.parent.range; - if (data && range) { - // TODO: account for the width of the item - var interval = (range.end - range.start); - this.visible = (data.start > range.start - interval) && (data.start < range.end); + var changed = 0, + update, + dom, + props, + options, + margin, + orientation, + start, + top, + data, + range; + + if (this.data.start == undefined) { + throw new Error('Property "start" missing in item ' + this.data.id); + } + + data = this.data; + range = this.parent && this.parent.range; + if (data && range) { + // TODO: account for the width of the item + var interval = (range.end - range.start); + this.visible = (data.start > range.start - interval) && (data.start < range.end); + } + else { + this.visible = false; + } + + if (this.visible) { + dom = this.dom; + if (dom) { + update = util.updateProperty; + props = this.props; + options = this.options; + orientation = options.orientation || this.defaultOptions.orientation; + margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis; + start = this.parent.toScreen(this.data.start); + + changed += update(this, 'width', dom.point.offsetWidth); + changed += update(this, 'height', dom.point.offsetHeight); + changed += update(props.dot, 'width', dom.dot.offsetWidth); + changed += update(props.dot, 'height', dom.dot.offsetHeight); + changed += update(props.content, 'height', dom.content.offsetHeight); + + if (orientation == 'top') { + top = margin; + } + else { + // default or 'bottom' + var parentHeight = this.parent.height; + top = Math.max(parentHeight - this.height - margin, 0); + } + changed += update(this, 'top', top); + changed += update(this, 'left', start - props.dot.width / 2); + changed += update(props.content, 'marginLeft', 1.5 * props.dot.width); + //changed += update(props.content, 'marginRight', 0.5 * props.dot.width); // TODO + + changed += update(props.dot, 'top', (this.height - props.dot.height) / 2); } else { - this.visible = false; + changed += 1; } + } - if (this.visible) { - dom = this.dom; - if (dom) { - update = util.updateProperty; - props = this.props; - options = this.options; - orientation = options.orientation || this.defaultOptions.orientation; - margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis; - start = this.parent.toScreen(this.data.start); - - changed += update(this, 'width', dom.point.offsetWidth); - changed += update(this, 'height', dom.point.offsetHeight); - changed += update(props.dot, 'width', dom.dot.offsetWidth); - changed += update(props.dot, 'height', dom.dot.offsetHeight); - changed += update(props.content, 'height', dom.content.offsetHeight); - - if (orientation == 'top') { - top = margin; - } - else { - // default or 'bottom' - var parentHeight = this.parent.height; - top = Math.max(parentHeight - this.height - margin, 0); - } - changed += update(this, 'top', top); - changed += update(this, 'left', start - props.dot.width / 2); - changed += update(props.content, 'marginLeft', 1.5 * props.dot.width); - //changed += update(props.content, 'marginRight', 0.5 * props.dot.width); // TODO - - changed += update(props.dot, 'top', (this.height - props.dot.height) / 2); - } - else { - changed += 1; - } - } - - return (changed > 0); + return (changed > 0); }; /** @@ -211,24 +211,24 @@ ItemPoint.prototype.reflow = function reflow() { * @private */ ItemPoint.prototype._create = function _create() { - var dom = this.dom; - if (!dom) { - this.dom = dom = {}; - - // background box - dom.point = document.createElement('div'); - // className is updated in repaint() - - // contents box, right from the dot - dom.content = document.createElement('div'); - dom.content.className = 'content'; - dom.point.appendChild(dom.content); - - // dot at start - dom.dot = document.createElement('div'); - dom.dot.className = 'dot'; - dom.point.appendChild(dom.dot); - } + var dom = this.dom; + if (!dom) { + this.dom = dom = {}; + + // background box + dom.point = document.createElement('div'); + // className is updated in repaint() + + // contents box, right from the dot + dom.content = document.createElement('div'); + dom.content.className = 'content'; + dom.point.appendChild(dom.content); + + // dot at start + dom.dot = document.createElement('div'); + dom.dot.className = 'dot'; + dom.point.appendChild(dom.dot); + } }; /** @@ -237,16 +237,16 @@ ItemPoint.prototype._create = function _create() { * @override */ ItemPoint.prototype.reposition = function reposition() { - var dom = this.dom, - props = this.props; + var dom = this.dom, + props = this.props; - if (dom) { - dom.point.style.top = this.top + 'px'; - dom.point.style.left = this.left + 'px'; + if (dom) { + dom.point.style.top = this.top + 'px'; + dom.point.style.left = this.left + 'px'; - dom.content.style.marginLeft = props.content.marginLeft + 'px'; - //dom.content.style.marginRight = props.content.marginRight + 'px'; // TODO + dom.content.style.marginLeft = props.content.marginLeft + 'px'; + //dom.content.style.marginRight = props.content.marginRight + 'px'; // TODO - dom.dot.style.top = props.dot.top + 'px'; - } + dom.dot.style.top = props.dot.top + 'px'; + } }; diff --git a/src/timeline/component/item/ItemRange.js b/src/timeline/component/item/ItemRange.js index c42ffc03..50c800c3 100644 --- a/src/timeline/component/item/ItemRange.js +++ b/src/timeline/component/item/ItemRange.js @@ -9,14 +9,14 @@ * // TODO: describe available options */ function ItemRange (parent, data, options, defaultOptions) { - this.props = { - content: { - left: 0, - width: 0 - } - }; + this.props = { + content: { + left: 0, + width: 0 + } + }; - Item.call(this, parent, data, options, defaultOptions); + Item.call(this, parent, data, options, defaultOptions); } ItemRange.prototype = new Item (null, null); @@ -26,8 +26,8 @@ ItemRange.prototype = new Item (null, null); * @override */ ItemRange.prototype.select = function select() { - this.selected = true; - // TODO: select and unselect + this.selected = true; + // TODO: select and unselect }; /** @@ -35,8 +35,8 @@ ItemRange.prototype.select = function select() { * @override */ ItemRange.prototype.unselect = function unselect() { - this.selected = false; - // TODO: select and unselect + this.selected = false; + // TODO: select and unselect }; /** @@ -44,57 +44,57 @@ ItemRange.prototype.unselect = function unselect() { * @return {Boolean} changed */ ItemRange.prototype.repaint = function repaint() { - // TODO: make an efficient repaint - var changed = false; - var dom = this.dom; + // TODO: make an efficient repaint + var changed = false; + var dom = this.dom; - if (!dom) { - this._create(); - dom = this.dom; - changed = true; - } + if (!dom) { + this._create(); + dom = this.dom; + changed = true; + } - if (dom) { - if (!this.parent) { - throw new Error('Cannot repaint item: no parent attached'); - } - var foreground = this.parent.getForeground(); - if (!foreground) { - throw new Error('Cannot repaint time axis: ' + - 'parent has no foreground container element'); - } + if (dom) { + if (!this.parent) { + throw new Error('Cannot repaint item: no parent attached'); + } + var foreground = this.parent.getForeground(); + if (!foreground) { + throw new Error('Cannot repaint time axis: ' + + 'parent has no foreground container element'); + } - if (!dom.box.parentNode) { - foreground.appendChild(dom.box); - changed = true; - } + if (!dom.box.parentNode) { + foreground.appendChild(dom.box); + changed = true; + } - // update content - if (this.data.content != this.content) { - this.content = this.data.content; - if (this.content instanceof Element) { - dom.content.innerHTML = ''; - dom.content.appendChild(this.content); - } - else if (this.data.content != undefined) { - dom.content.innerHTML = this.content; - } - else { - throw new Error('Property "content" missing in item ' + this.data.id); - } - changed = true; - } + // update content + if (this.data.content != this.content) { + this.content = this.data.content; + if (this.content instanceof Element) { + dom.content.innerHTML = ''; + dom.content.appendChild(this.content); + } + else if (this.data.content != undefined) { + dom.content.innerHTML = this.content; + } + else { + throw new Error('Property "content" missing in item ' + this.data.id); + } + changed = true; + } - // update class - var className = this.data.className ? (' ' + this.data.className) : ''; - if (this.className != className) { - this.className = className; - dom.box.className = 'item range' + className; - changed = true; - } + // update class + var className = this.data.className ? (' ' + this.data.className) : ''; + if (this.className != className) { + this.className = className; + dom.box.className = 'item range' + className; + changed = true; } + } - return changed; + return changed; }; /** @@ -103,12 +103,12 @@ ItemRange.prototype.repaint = function repaint() { * @return {Boolean} changed */ ItemRange.prototype.show = function show() { - if (!this.dom || !this.dom.box.parentNode) { - return this.repaint(); - } - else { - return false; - } + if (!this.dom || !this.dom.box.parentNode) { + return this.repaint(); + } + else { + return false; + } }; /** @@ -116,15 +116,15 @@ ItemRange.prototype.show = function show() { * @return {Boolean} changed */ ItemRange.prototype.hide = function hide() { - var changed = false, - dom = this.dom; - if (dom) { - if (dom.box.parentNode) { - dom.box.parentNode.removeChild(dom.box); - changed = true; - } + var changed = false, + dom = this.dom; + if (dom) { + if (dom.box.parentNode) { + dom.box.parentNode.removeChild(dom.box); + changed = true; } - return changed; + } + return changed; }; /** @@ -133,98 +133,98 @@ ItemRange.prototype.hide = function hide() { * @override */ ItemRange.prototype.reflow = function reflow() { - var changed = 0, - dom, - props, - options, - margin, - padding, - parent, - start, - end, - data, - range, - update, - box, - parentWidth, - contentLeft, - orientation, - top; + var changed = 0, + dom, + props, + options, + margin, + padding, + parent, + start, + end, + data, + range, + update, + box, + parentWidth, + contentLeft, + orientation, + top; - if (this.data.start == undefined) { - throw new Error('Property "start" missing in item ' + this.data.id); - } - if (this.data.end == undefined) { - throw new Error('Property "end" missing in item ' + this.data.id); - } + if (this.data.start == undefined) { + throw new Error('Property "start" missing in item ' + this.data.id); + } + if (this.data.end == undefined) { + throw new Error('Property "end" missing in item ' + this.data.id); + } - data = this.data; - range = this.parent && this.parent.range; - if (data && range) { - // TODO: account for the width of the item. Take some margin - this.visible = (data.start < range.end) && (data.end > range.start); - } - else { - this.visible = false; - } + data = this.data; + range = this.parent && this.parent.range; + if (data && range) { + // TODO: account for the width of the item. Take some margin + this.visible = (data.start < range.end) && (data.end > range.start); + } + else { + this.visible = false; + } - if (this.visible) { - dom = this.dom; - if (dom) { - props = this.props; - options = this.options; - parent = this.parent; - start = parent.toScreen(this.data.start); - end = parent.toScreen(this.data.end); - update = util.updateProperty; - box = dom.box; - parentWidth = parent.width; - orientation = options.orientation || this.defaultOptions.orientation; - margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis; - padding = options.padding || this.defaultOptions.padding; + if (this.visible) { + dom = this.dom; + if (dom) { + props = this.props; + options = this.options; + parent = this.parent; + start = parent.toScreen(this.data.start); + end = parent.toScreen(this.data.end); + update = util.updateProperty; + box = dom.box; + parentWidth = parent.width; + orientation = options.orientation || this.defaultOptions.orientation; + margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis; + padding = options.padding || this.defaultOptions.padding; - changed += update(props.content, 'width', dom.content.offsetWidth); + changed += update(props.content, 'width', dom.content.offsetWidth); - changed += update(this, 'height', box.offsetHeight); + changed += update(this, 'height', box.offsetHeight); - // limit the width of the this, as browsers cannot draw very wide divs - if (start < -parentWidth) { - start = -parentWidth; - } - if (end > 2 * parentWidth) { - end = 2 * parentWidth; - } + // limit the width of the this, as browsers cannot draw very wide divs + if (start < -parentWidth) { + start = -parentWidth; + } + if (end > 2 * parentWidth) { + end = 2 * parentWidth; + } - // when range exceeds left of the window, position the contents at the left of the visible area - if (start < 0) { - contentLeft = Math.min(-start, - (end - start - props.content.width - 2 * padding)); - // TODO: remove the need for options.padding. it's terrible. - } - else { - contentLeft = 0; - } - changed += update(props.content, 'left', contentLeft); + // when range exceeds left of the window, position the contents at the left of the visible area + if (start < 0) { + contentLeft = Math.min(-start, + (end - start - props.content.width - 2 * padding)); + // TODO: remove the need for options.padding. it's terrible. + } + else { + contentLeft = 0; + } + changed += update(props.content, 'left', contentLeft); - if (orientation == 'top') { - top = margin; - changed += update(this, 'top', top); - } - else { - // default or 'bottom' - top = parent.height - this.height - margin; - changed += update(this, 'top', top); - } + if (orientation == 'top') { + top = margin; + changed += update(this, 'top', top); + } + else { + // default or 'bottom' + top = parent.height - this.height - margin; + changed += update(this, 'top', top); + } - changed += update(this, 'left', start); - changed += update(this, 'width', Math.max(end - start, 1)); // TODO: reckon with border width; - } - else { - changed += 1; - } + changed += update(this, 'left', start); + changed += update(this, 'width', Math.max(end - start, 1)); // TODO: reckon with border width; } + else { + changed += 1; + } + } - return (changed > 0); + return (changed > 0); }; /** @@ -232,18 +232,18 @@ ItemRange.prototype.reflow = function reflow() { * @private */ ItemRange.prototype._create = function _create() { - var dom = this.dom; - if (!dom) { - this.dom = dom = {}; - // background box - dom.box = document.createElement('div'); - // className is updated in repaint() + var dom = this.dom; + if (!dom) { + this.dom = dom = {}; + // background box + dom.box = document.createElement('div'); + // className is updated in repaint() - // contents box - dom.content = document.createElement('div'); - dom.content.className = 'content'; - dom.box.appendChild(dom.content); - } + // contents box + dom.content = document.createElement('div'); + dom.content.className = 'content'; + dom.box.appendChild(dom.content); + } }; /** @@ -252,14 +252,14 @@ ItemRange.prototype._create = function _create() { * @override */ ItemRange.prototype.reposition = function reposition() { - var dom = this.dom, - props = this.props; + var dom = this.dom, + props = this.props; - if (dom) { - dom.box.style.top = this.top + 'px'; - dom.box.style.left = this.left + 'px'; - dom.box.style.width = this.width + 'px'; + if (dom) { + dom.box.style.top = this.top + 'px'; + dom.box.style.left = this.left + 'px'; + dom.box.style.width = this.width + 'px'; - dom.content.style.left = props.content.left + 'px'; - } + dom.content.style.left = props.content.left + 'px'; + } }; diff --git a/src/timeline/component/item/ItemRangeOverflow.js b/src/timeline/component/item/ItemRangeOverflow.js index 521d42aa..4e0d4cbb 100644 --- a/src/timeline/component/item/ItemRangeOverflow.js +++ b/src/timeline/component/item/ItemRangeOverflow.js @@ -9,14 +9,14 @@ * // TODO: describe available options */ function ItemRangeOverflow (parent, data, options, defaultOptions) { - this.props = { - content: { - left: 0, - width: 0 - } - }; + this.props = { + content: { + left: 0, + width: 0 + } + }; - ItemRange.call(this, parent, data, options, defaultOptions); + ItemRange.call(this, parent, data, options, defaultOptions); } ItemRangeOverflow.prototype = new ItemRange (null, null); @@ -26,57 +26,57 @@ ItemRangeOverflow.prototype = new ItemRange (null, null); * @return {Boolean} changed */ ItemRangeOverflow.prototype.repaint = function repaint() { - // TODO: make an efficient repaint - var changed = false; - var dom = this.dom; + // TODO: make an efficient repaint + var changed = false; + var dom = this.dom; - if (!dom) { - this._create(); - dom = this.dom; - changed = true; - } + if (!dom) { + this._create(); + dom = this.dom; + changed = true; + } - if (dom) { - if (!this.parent) { - throw new Error('Cannot repaint item: no parent attached'); - } - var foreground = this.parent.getForeground(); - if (!foreground) { - throw new Error('Cannot repaint time axis: ' + - 'parent has no foreground container element'); - } + if (dom) { + if (!this.parent) { + throw new Error('Cannot repaint item: no parent attached'); + } + var foreground = this.parent.getForeground(); + if (!foreground) { + throw new Error('Cannot repaint time axis: ' + + 'parent has no foreground container element'); + } - if (!dom.box.parentNode) { - foreground.appendChild(dom.box); - changed = true; - } + if (!dom.box.parentNode) { + foreground.appendChild(dom.box); + changed = true; + } - // update content - if (this.data.content != this.content) { - this.content = this.data.content; - if (this.content instanceof Element) { - dom.content.innerHTML = ''; - dom.content.appendChild(this.content); - } - else if (this.data.content != undefined) { - dom.content.innerHTML = this.content; - } - else { - throw new Error('Property "content" missing in item ' + this.data.id); - } - changed = true; - } + // update content + if (this.data.content != this.content) { + this.content = this.data.content; + if (this.content instanceof Element) { + dom.content.innerHTML = ''; + dom.content.appendChild(this.content); + } + else if (this.data.content != undefined) { + dom.content.innerHTML = this.content; + } + else { + throw new Error('Property "content" missing in item ' + this.data.id); + } + changed = true; + } - // update class - var className = this.data.className ? (' ' + this.data.className) : ''; - if (this.className != className) { - this.className = className; - dom.box.className = 'item rangeoverflow' + className; - changed = true; - } + // update class + var className = this.data.className ? (' ' + this.data.className) : ''; + if (this.className != className) { + this.className = className; + dom.box.className = 'item rangeoverflow' + className; + changed = true; } + } - return changed; + return changed; }; /** @@ -84,8 +84,8 @@ ItemRangeOverflow.prototype.repaint = function repaint() { * @return {Number} width */ ItemRangeOverflow.prototype.getWidth = function getWidth() { - if (this.props.content !== undefined && this.width < this.props.content.width) - return this.props.content.width; - else - return this.width; + if (this.props.content !== undefined && this.width < this.props.content.width) + return this.props.content.width; + else + return this.width; }; diff --git a/src/util.js b/src/util.js index 95dcafbc..e7abbf30 100644 --- a/src/util.js +++ b/src/util.js @@ -9,7 +9,7 @@ var util = {}; * @return {Boolean} isNumber */ util.isNumber = function isNumber(object) { - return (object instanceof Number || typeof object == 'number'); + return (object instanceof Number || typeof object == 'number'); }; /** @@ -18,7 +18,7 @@ util.isNumber = function isNumber(object) { * @return {Boolean} isString */ util.isString = function isString(object) { - return (object instanceof String || typeof object == 'string'); + return (object instanceof String || typeof object == 'string'); }; /** @@ -27,21 +27,21 @@ util.isString = function isString(object) { * @return {Boolean} isDate */ util.isDate = function isDate(object) { - if (object instanceof Date) { - return true; + if (object instanceof Date) { + return true; + } + else if (util.isString(object)) { + // test whether this string contains a date + var match = ASPDateRegex.exec(object); + if (match) { + return true; } - else if (util.isString(object)) { - // test whether this string contains a date - var match = ASPDateRegex.exec(object); - if (match) { - return true; - } - else if (!isNaN(Date.parse(object))) { - return true; - } + else if (!isNaN(Date.parse(object))) { + return true; } + } - return false; + return false; }; /** @@ -50,10 +50,10 @@ util.isDate = function isDate(object) { * @return {Boolean} isDataTable */ util.isDataTable = function isDataTable(object) { - return (typeof (google) !== 'undefined') && - (google.visualization) && - (google.visualization.DataTable) && - (object instanceof google.visualization.DataTable); + return (typeof (google) !== 'undefined') && + (google.visualization) && + (google.visualization.DataTable) && + (object instanceof google.visualization.DataTable); }; /** @@ -62,19 +62,19 @@ util.isDataTable = function isDataTable(object) { * @return {String} uuid */ util.randomUUID = function randomUUID () { - var S4 = function () { - return Math.floor( - Math.random() * 0x10000 /* 65536 */ - ).toString(16); - }; - - return ( - S4() + S4() + '-' + - S4() + '-' + - S4() + '-' + - S4() + '-' + - S4() + S4() + S4() - ); + var S4 = function () { + return Math.floor( + Math.random() * 0x10000 /* 65536 */ + ).toString(16); + }; + + return ( + S4() + S4() + '-' + + S4() + '-' + + S4() + '-' + + S4() + '-' + + S4() + S4() + S4() + ); }; /** @@ -85,16 +85,16 @@ util.randomUUID = function randomUUID () { * @return {Object} a */ util.extend = function (a, b) { - for (var i = 1, len = arguments.length; i < len; i++) { - var other = arguments[i]; - for (var prop in other) { - if (other.hasOwnProperty(prop) && other[prop] !== undefined) { - a[prop] = other[prop]; - } - } + for (var i = 1, len = arguments.length; i < len; i++) { + var other = arguments[i]; + for (var prop in other) { + if (other.hasOwnProperty(prop) && other[prop] !== undefined) { + a[prop] = other[prop]; + } } + } - return a; + return a; }; /** @@ -107,143 +107,143 @@ util.extend = function (a, b) { * @throws Error */ util.convert = function convert(object, type) { - var match; - - if (object === undefined) { - return undefined; - } - if (object === null) { - return null; - } - - if (!type) { - return object; - } - if (!(typeof type === 'string') && !(type instanceof String)) { - throw new Error('Type must be a string'); - } - - //noinspection FallthroughInSwitchStatementJS - switch (type) { - case 'boolean': - case 'Boolean': - return Boolean(object); - - case 'number': - case 'Number': - return Number(object.valueOf()); - - case 'string': - case 'String': - return String(object); - - case 'Date': - if (util.isNumber(object)) { - return new Date(object); - } - if (object instanceof Date) { - return new Date(object.valueOf()); - } - else if (moment.isMoment(object)) { - return new Date(object.valueOf()); - } - if (util.isString(object)) { - match = ASPDateRegex.exec(object); - if (match) { - // object is an ASP date - return new Date(Number(match[1])); // parse number - } - else { - return moment(object).toDate(); // parse string - } - } - else { - throw new Error( - 'Cannot convert object of type ' + util.getType(object) + - ' to type Date'); - } - - case 'Moment': - if (util.isNumber(object)) { - return moment(object); - } - if (object instanceof Date) { - return moment(object.valueOf()); - } - else if (moment.isMoment(object)) { - return moment(object); - } - if (util.isString(object)) { - match = ASPDateRegex.exec(object); - if (match) { - // object is an ASP date - return moment(Number(match[1])); // parse number - } - else { - return moment(object); // parse string - } - } - else { - throw new Error( - 'Cannot convert object of type ' + util.getType(object) + - ' to type Date'); - } - - case 'ISODate': - if (util.isNumber(object)) { - return new Date(object); - } - else if (object instanceof Date) { - return object.toISOString(); - } - else if (moment.isMoment(object)) { - return object.toDate().toISOString(); - } - else if (util.isString(object)) { - match = ASPDateRegex.exec(object); - if (match) { - // object is an ASP date - return new Date(Number(match[1])).toISOString(); // parse number - } - else { - return new Date(object).toISOString(); // parse string - } - } - else { - throw new Error( - 'Cannot convert object of type ' + util.getType(object) + - ' to type ISODate'); - } - - case 'ASPDate': - if (util.isNumber(object)) { - return '/Date(' + object + ')/'; - } - else if (object instanceof Date) { - return '/Date(' + object.valueOf() + ')/'; - } - else if (util.isString(object)) { - match = ASPDateRegex.exec(object); - var value; - if (match) { - // object is an ASP date - value = new Date(Number(match[1])).valueOf(); // parse number - } - else { - value = new Date(object).valueOf(); // parse string - } - return '/Date(' + value + ')/'; - } - else { - throw new Error( - 'Cannot convert object of type ' + util.getType(object) + - ' to type ASPDate'); - } - - default: - throw new Error('Cannot convert object of type ' + util.getType(object) + - ' to type "' + type + '"'); - } + var match; + + if (object === undefined) { + return undefined; + } + if (object === null) { + return null; + } + + if (!type) { + return object; + } + if (!(typeof type === 'string') && !(type instanceof String)) { + throw new Error('Type must be a string'); + } + + //noinspection FallthroughInSwitchStatementJS + switch (type) { + case 'boolean': + case 'Boolean': + return Boolean(object); + + case 'number': + case 'Number': + return Number(object.valueOf()); + + case 'string': + case 'String': + return String(object); + + case 'Date': + if (util.isNumber(object)) { + return new Date(object); + } + if (object instanceof Date) { + return new Date(object.valueOf()); + } + else if (moment.isMoment(object)) { + return new Date(object.valueOf()); + } + if (util.isString(object)) { + match = ASPDateRegex.exec(object); + if (match) { + // object is an ASP date + return new Date(Number(match[1])); // parse number + } + else { + return moment(object).toDate(); // parse string + } + } + else { + throw new Error( + 'Cannot convert object of type ' + util.getType(object) + + ' to type Date'); + } + + case 'Moment': + if (util.isNumber(object)) { + return moment(object); + } + if (object instanceof Date) { + return moment(object.valueOf()); + } + else if (moment.isMoment(object)) { + return moment(object); + } + if (util.isString(object)) { + match = ASPDateRegex.exec(object); + if (match) { + // object is an ASP date + return moment(Number(match[1])); // parse number + } + else { + return moment(object); // parse string + } + } + else { + throw new Error( + 'Cannot convert object of type ' + util.getType(object) + + ' to type Date'); + } + + case 'ISODate': + if (util.isNumber(object)) { + return new Date(object); + } + else if (object instanceof Date) { + return object.toISOString(); + } + else if (moment.isMoment(object)) { + return object.toDate().toISOString(); + } + else if (util.isString(object)) { + match = ASPDateRegex.exec(object); + if (match) { + // object is an ASP date + return new Date(Number(match[1])).toISOString(); // parse number + } + else { + return new Date(object).toISOString(); // parse string + } + } + else { + throw new Error( + 'Cannot convert object of type ' + util.getType(object) + + ' to type ISODate'); + } + + case 'ASPDate': + if (util.isNumber(object)) { + return '/Date(' + object + ')/'; + } + else if (object instanceof Date) { + return '/Date(' + object.valueOf() + ')/'; + } + else if (util.isString(object)) { + match = ASPDateRegex.exec(object); + var value; + if (match) { + // object is an ASP date + value = new Date(Number(match[1])).valueOf(); // parse number + } + else { + value = new Date(object).valueOf(); // parse string + } + return '/Date(' + value + ')/'; + } + else { + throw new Error( + 'Cannot convert object of type ' + util.getType(object) + + ' to type ASPDate'); + } + + default: + throw new Error('Cannot convert object of type ' + util.getType(object) + + ' to type "' + type + '"'); + } }; // parse ASP.Net Date pattern, @@ -257,40 +257,40 @@ var ASPDateRegex = /^\/?Date\((\-?\d+)/i; * @return {String} type */ util.getType = function getType(object) { - var type = typeof object; + var type = typeof object; - if (type == 'object') { - if (object == null) { - return 'null'; - } - if (object instanceof Boolean) { - return 'Boolean'; - } - if (object instanceof Number) { - return 'Number'; - } - if (object instanceof String) { - return 'String'; - } - if (object instanceof Array) { - return 'Array'; - } - if (object instanceof Date) { - return 'Date'; - } - return 'Object'; + if (type == 'object') { + if (object == null) { + return 'null'; } - else if (type == 'number') { - return 'Number'; + if (object instanceof Boolean) { + return 'Boolean'; } - else if (type == 'boolean') { - return 'Boolean'; + if (object instanceof Number) { + return 'Number'; } - else if (type == 'string') { - return 'String'; + if (object instanceof String) { + return 'String'; } - - return type; + if (object instanceof Array) { + return 'Array'; + } + if (object instanceof Date) { + return 'Date'; + } + return 'Object'; + } + else if (type == 'number') { + return 'Number'; + } + else if (type == 'boolean') { + return 'Boolean'; + } + else if (type == 'string') { + return 'String'; + } + + return type; }; /** @@ -300,17 +300,17 @@ util.getType = function getType(object) { * in the browser page. */ util.getAbsoluteLeft = function getAbsoluteLeft (elem) { - var doc = document.documentElement; - var body = document.body; - - var left = elem.offsetLeft; - var e = elem.offsetParent; - while (e != null && e != body && e != doc) { - left += e.offsetLeft; - left -= e.scrollLeft; - e = e.offsetParent; - } - return left; + var doc = document.documentElement; + var body = document.body; + + var left = elem.offsetLeft; + var e = elem.offsetParent; + while (e != null && e != body && e != doc) { + left += e.offsetLeft; + left -= e.scrollLeft; + e = e.offsetParent; + } + return left; }; /** @@ -320,17 +320,17 @@ util.getAbsoluteLeft = function getAbsoluteLeft (elem) { * in the browser page. */ util.getAbsoluteTop = function getAbsoluteTop (elem) { - var doc = document.documentElement; - var body = document.body; - - var top = elem.offsetTop; - var e = elem.offsetParent; - while (e != null && e != body && e != doc) { - top += e.offsetTop; - top -= e.scrollTop; - e = e.offsetParent; - } - return top; + var doc = document.documentElement; + var body = document.body; + + var top = elem.offsetTop; + var e = elem.offsetParent; + while (e != null && e != body && e != doc) { + top += e.offsetTop; + top -= e.scrollTop; + e = e.offsetParent; + } + return top; }; /** @@ -339,24 +339,24 @@ util.getAbsoluteTop = function getAbsoluteTop (elem) { * @return {Number} pageY */ util.getPageY = function getPageY (event) { - if ('pageY' in event) { - return event.pageY; + if ('pageY' in event) { + return event.pageY; + } + else { + var clientY; + if (('targetTouches' in event) && event.targetTouches.length) { + clientY = event.targetTouches[0].clientY; } else { - var clientY; - if (('targetTouches' in event) && event.targetTouches.length) { - clientY = event.targetTouches[0].clientY; - } - else { - clientY = event.clientY; - } - - var doc = document.documentElement; - var body = document.body; - return clientY + - ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - - ( doc && doc.clientTop || body && body.clientTop || 0 ); + clientY = event.clientY; } + + var doc = document.documentElement; + var body = document.body; + return clientY + + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - + ( doc && doc.clientTop || body && body.clientTop || 0 ); + } }; /** @@ -365,24 +365,24 @@ util.getPageY = function getPageY (event) { * @return {Number} pageX */ util.getPageX = function getPageX (event) { - if ('pageY' in event) { - return event.pageX; + if ('pageY' in event) { + return event.pageX; + } + else { + var clientX; + if (('targetTouches' in event) && event.targetTouches.length) { + clientX = event.targetTouches[0].clientX; } else { - var clientX; - if (('targetTouches' in event) && event.targetTouches.length) { - clientX = event.targetTouches[0].clientX; - } - else { - clientX = event.clientX; - } - - var doc = document.documentElement; - var body = document.body; - return clientX + - ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + clientX = event.clientX; } + + var doc = document.documentElement; + var body = document.body; + return clientX + + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - + ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + } }; /** @@ -391,11 +391,11 @@ util.getPageX = function getPageX (event) { * @param {String} className */ util.addClassName = function addClassName(elem, className) { - var classes = elem.className.split(' '); - if (classes.indexOf(className) == -1) { - classes.push(className); // add the class to the array - elem.className = classes.join(' '); - } + var classes = elem.className.split(' '); + if (classes.indexOf(className) == -1) { + classes.push(className); // add the class to the array + elem.className = classes.join(' '); + } }; /** @@ -404,12 +404,12 @@ util.addClassName = function addClassName(elem, className) { * @param {String} className */ util.removeClassName = function removeClassname(elem, className) { - var classes = elem.className.split(' '); - var index = classes.indexOf(className); - if (index != -1) { - classes.splice(index, 1); // remove the class from the array - elem.className = classes.join(' '); - } + var classes = elem.className.split(' '); + var index = classes.indexOf(className); + if (index != -1) { + classes.splice(index, 1); // remove the class from the array + elem.className = classes.join(' '); + } }; /** @@ -422,22 +422,22 @@ util.removeClassName = function removeClassname(elem, className) { * callback(value, index, object) */ util.forEach = function forEach (object, callback) { - var i, - len; - if (object instanceof Array) { - // array - for (i = 0, len = object.length; i < len; i++) { - callback(object[i], i, object); - } - } - else { - // object - for (i in object) { - if (object.hasOwnProperty(i)) { - callback(object[i], i, object); - } - } - } + var i, + len; + if (object instanceof Array) { + // array + for (i = 0, len = object.length; i < len; i++) { + callback(object[i], i, object); + } + } + else { + // object + for (i in object) { + if (object.hasOwnProperty(i)) { + callback(object[i], i, object); + } + } + } }; /** @@ -448,13 +448,13 @@ util.forEach = function forEach (object, callback) { * @return {Boolean} changed */ util.updateProperty = function updateProp (object, key, value) { - if (object[key] !== value) { - object[key] = value; - return true; - } - else { - return false; - } + if (object[key] !== value) { + object[key] = value; + return true; + } + else { + return false; + } }; /** @@ -466,18 +466,18 @@ util.updateProperty = function updateProp (object, key, value) { * @param {boolean} [useCapture] */ util.addEventListener = function addEventListener(element, action, listener, useCapture) { - if (element.addEventListener) { - if (useCapture === undefined) - useCapture = false; + if (element.addEventListener) { + if (useCapture === undefined) + useCapture = false; - if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { - action = "DOMMouseScroll"; // For Firefox - } - - element.addEventListener(action, listener, useCapture); - } else { - element.attachEvent("on" + action, listener); // IE browsers + if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { + action = "DOMMouseScroll"; // For Firefox } + + element.addEventListener(action, listener, useCapture); + } else { + element.attachEvent("on" + action, listener); // IE browsers + } }; /** @@ -488,20 +488,20 @@ util.addEventListener = function addEventListener(element, action, listener, use * @param {boolean} [useCapture] */ util.removeEventListener = function removeEventListener(element, action, listener, useCapture) { - if (element.removeEventListener) { - // non-IE browsers - if (useCapture === undefined) - useCapture = false; + if (element.removeEventListener) { + // non-IE browsers + if (useCapture === undefined) + useCapture = false; - if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { - action = "DOMMouseScroll"; // For Firefox - } - - element.removeEventListener(action, listener, useCapture); - } else { - // IE browsers - element.detachEvent("on" + action, listener); + if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { + action = "DOMMouseScroll"; // For Firefox } + + element.removeEventListener(action, listener, useCapture); + } else { + // IE browsers + element.detachEvent("on" + action, listener); + } }; @@ -511,41 +511,41 @@ util.removeEventListener = function removeEventListener(element, action, listene * @return {Element} target element */ util.getTarget = function getTarget(event) { - // code from http://www.quirksmode.org/js/events_properties.html - if (!event) { - event = window.event; - } - - var target; - - if (event.target) { - target = event.target; - } - else if (event.srcElement) { - target = event.srcElement; - } - - if (target.nodeType != undefined && target.nodeType == 3) { - // defeat Safari bug - target = target.parentNode; - } - - return target; + // code from http://www.quirksmode.org/js/events_properties.html + if (!event) { + event = window.event; + } + + var target; + + if (event.target) { + target = event.target; + } + else if (event.srcElement) { + target = event.srcElement; + } + + if (target.nodeType != undefined && target.nodeType == 3) { + // defeat Safari bug + target = target.parentNode; + } + + return target; }; /** * Stop event propagation */ util.stopPropagation = function stopPropagation(event) { - if (!event) - event = window.event; - - if (event.stopPropagation) { - event.stopPropagation(); // non-IE browsers - } - else { - event.cancelBubble = true; // IE browsers - } + if (!event) + event = window.event; + + if (event.stopPropagation) { + event.stopPropagation(); // non-IE browsers + } + else { + event.cancelBubble = true; // IE browsers + } }; @@ -553,15 +553,15 @@ util.stopPropagation = function stopPropagation(event) { * Cancels the event if it is cancelable, without stopping further propagation of the event. */ util.preventDefault = function preventDefault (event) { - if (!event) - event = window.event; - - if (event.preventDefault) { - event.preventDefault(); // non-IE browsers - } - else { - event.returnValue = false; // IE browsers - } + if (!event) + event = window.event; + + if (event.preventDefault) { + event.preventDefault(); // non-IE browsers + } + else { + event.returnValue = false; // IE browsers + } }; @@ -574,15 +574,15 @@ util.option = {}; * @returns {Boolean} bool */ util.option.asBoolean = function (value, defaultValue) { - if (typeof value == 'function') { - value = value(); - } + if (typeof value == 'function') { + value = value(); + } - if (value != null) { - return (value != false); - } + if (value != null) { + return (value != false); + } - return defaultValue || null; + return defaultValue || null; }; /** @@ -592,15 +592,15 @@ util.option.asBoolean = function (value, defaultValue) { * @returns {Number} number */ util.option.asNumber = function (value, defaultValue) { - if (typeof value == 'function') { - value = value(); - } + if (typeof value == 'function') { + value = value(); + } - if (value != null) { - return Number(value) || defaultValue || null; - } + if (value != null) { + return Number(value) || defaultValue || null; + } - return defaultValue || null; + return defaultValue || null; }; /** @@ -610,15 +610,15 @@ util.option.asNumber = function (value, defaultValue) { * @returns {String} str */ util.option.asString = function (value, defaultValue) { - if (typeof value == 'function') { - value = value(); - } + if (typeof value == 'function') { + value = value(); + } - if (value != null) { - return String(value); - } + if (value != null) { + return String(value); + } - return defaultValue || null; + return defaultValue || null; }; /** @@ -628,19 +628,19 @@ util.option.asString = function (value, defaultValue) { * @returns {String} size */ util.option.asSize = function (value, defaultValue) { - if (typeof value == 'function') { - value = value(); - } - - if (util.isString(value)) { - return value; - } - else if (util.isNumber(value)) { - return value + 'px'; - } - else { - return defaultValue || null; - } + if (typeof value == 'function') { + value = value(); + } + + if (util.isString(value)) { + return value; + } + else if (util.isNumber(value)) { + return value + 'px'; + } + else { + return defaultValue || null; + } }; /** @@ -650,11 +650,11 @@ util.option.asSize = function (value, defaultValue) { * @returns {HTMLElement | null} dom */ util.option.asElement = function (value, defaultValue) { - if (typeof value == 'function') { - value = value(); - } + if (typeof value == 'function') { + value = value(); + } - return value || defaultValue || null; + return value || defaultValue || null; }; /** @@ -662,25 +662,25 @@ util.option.asElement = function (value, defaultValue) { * @param {String} css Text containing css */ util.loadCss = function (css) { - if (typeof document === 'undefined') { - return; - } - - // get the script location, and built the css file name from the js file name - // http://stackoverflow.com/a/2161748/1262753 - // var scripts = document.getElementsByTagName('script'); - // var jsFile = scripts[scripts.length-1].src.split('?')[0]; - // var cssFile = jsFile.substring(0, jsFile.length - 2) + 'css'; - - // inject css - // http://stackoverflow.com/questions/524696/how-to-create-a-style-tag-with-javascript - var style = document.createElement('style'); - style.type = 'text/css'; - if (style.styleSheet){ - style.styleSheet.cssText = css; - } else { - style.appendChild(document.createTextNode(css)); - } - - document.getElementsByTagName('head')[0].appendChild(style); + if (typeof document === 'undefined') { + return; + } + + // get the script location, and built the css file name from the js file name + // http://stackoverflow.com/a/2161748/1262753 + // var scripts = document.getElementsByTagName('script'); + // var jsFile = scripts[scripts.length-1].src.split('?')[0]; + // var cssFile = jsFile.substring(0, jsFile.length - 2) + 'css'; + + // inject css + // http://stackoverflow.com/questions/524696/how-to-create-a-style-tag-with-javascript + var style = document.createElement('style'); + style.type = 'text/css'; + if (style.styleSheet){ + style.styleSheet.cssText = css; + } else { + style.appendChild(document.createTextNode(css)); + } + + document.getElementsByTagName('head')[0].appendChild(style); }; diff --git a/test/dataset.html b/test/dataset.html index 7cb93ee6..ceff8219 100644 --- a/test/dataset.html +++ b/test/dataset.html @@ -1,75 +1,75 @@ - - - + + + - + \ No newline at end of file diff --git a/test/dataset.js b/test/dataset.js index 649127fc..63446918 100644 --- a/test/dataset.js +++ b/test/dataset.js @@ -6,81 +6,81 @@ var assert = require('assert'), var now = new Date(); var data = new DataSet({ - convert: { - start: 'Date', - end: 'Date' - } + convert: { + start: 'Date', + end: 'Date' + } }); // add single items with different date types data.add({id: 1, content: 'Item 1', start: new Date(now.valueOf())}); data.add({id: 2, content: 'Item 2', start: now.toISOString()}); data.add([ - //{id: 3, content: 'Item 3', start: moment(now)}, // TODO: moment fails, not the same instance - {id: 3, content: 'Item 3', start: now}, - {id: 4, content: 'Item 4', start: '/Date(' + now.valueOf() + ')/'} + //{id: 3, content: 'Item 3', start: moment(now)}, // TODO: moment fails, not the same instance + {id: 3, content: 'Item 3', start: now}, + {id: 4, content: 'Item 4', start: '/Date(' + now.valueOf() + ')/'} ]); var items = data.get(); assert.equal(items.length, 4); items.forEach(function (item) { - assert.ok(item.start instanceof Date); + assert.ok(item.start instanceof Date); }); // get filtered fields only var sort = function (a, b) { - return a.id > b.id; + return a.id > b.id; }; assert.deepEqual(data.get({ - fields: ['id', 'content'] + fields: ['id', 'content'] }).sort(sort), [ - {id: 1, content: 'Item 1'}, - {id: 2, content: 'Item 2'}, - {id: 3, content: 'Item 3'}, - {id: 4, content: 'Item 4'} + {id: 1, content: 'Item 1'}, + {id: 2, content: 'Item 2'}, + {id: 3, content: 'Item 3'}, + {id: 4, content: 'Item 4'} ]); // convert dates assert.deepEqual(data.get({ - fields: ['id', 'start'], - convert: {start: 'Number'} + fields: ['id', 'start'], + convert: {start: 'Number'} }).sort(sort), [ - {id: 1, start: now.valueOf()}, - {id: 2, start: now.valueOf()}, - {id: 3, start: now.valueOf()}, - {id: 4, start: now.valueOf()} + {id: 1, start: now.valueOf()}, + {id: 2, start: now.valueOf()}, + {id: 3, start: now.valueOf()}, + {id: 4, start: now.valueOf()} ]); // get a single item assert.deepEqual(data.get(1, { - fields: ['id', 'start'], - convert: {start: 'ISODate'} + fields: ['id', 'start'], + convert: {start: 'ISODate'} }), { - id: 1, - start: now.toISOString() + id: 1, + start: now.toISOString() }); // remove an item data.remove(2); assert.deepEqual(data.get({ - fields: ['id'] + fields: ['id'] }).sort(sort), [ - {id: 1}, - {id: 3}, - {id: 4} + {id: 1}, + {id: 3}, + {id: 4} ]); // add an item data.add({id: 5, content: 'Item 5', start: now.valueOf()}); assert.deepEqual(data.get({ - fields: ['id'] + fields: ['id'] }).sort(sort), [ - {id: 1}, - {id: 3}, - {id: 4}, - {id: 5} + {id: 1}, + {id: 3}, + {id: 4}, + {id: 5} ]); // update an item @@ -89,11 +89,11 @@ data.remove(3); // remove exi data.add({id: 3, other: 'bla'}); // add new item data.update({id: 6, content: 'created!', start: now.valueOf()}); // this item is not yet existing, create it assert.deepEqual(data.get().sort(sort), [ - {id: 1, content: 'Item 1', start: now}, - {id: 3, other: 'bla'}, - {id: 4, content: 'Item 4', start: now}, - {id: 5, content: 'changed!', start: now}, - {id: 6, content: 'created!', start: now} + {id: 1, content: 'Item 1', start: now}, + {id: 3, other: 'bla'}, + {id: 4, content: 'Item 4', start: now}, + {id: 5, content: 'changed!', start: now}, + {id: 6, content: 'created!', start: now} ]); data.clear(); @@ -104,42 +104,42 @@ assert.equal(data.get().length, 0); // test filtering and sorting data = new vis.DataSet(); data.add([ - {id:1, age: 30, group: 2}, - {id:2, age: 25, group: 4}, - {id:3, age: 17, group: 2}, - {id:4, age: 27, group: 3} + {id:1, age: 30, group: 2}, + {id:2, age: 25, group: 4}, + {id:3, age: 17, group: 2}, + {id:4, age: 27, group: 3} ]); assert.deepEqual(data.get({order: 'age'}), [ - {id:3, age: 17, group: 2}, - {id:2, age: 25, group: 4}, - {id:4, age: 27, group: 3}, - {id:1, age: 30, group: 2} + {id:3, age: 17, group: 2}, + {id:2, age: 25, group: 4}, + {id:4, age: 27, group: 3}, + {id:1, age: 30, group: 2} ]); assert.deepEqual(data.getIds({order: 'age'}), [3,2,4,1]); assert.deepEqual(data.get({order: 'age', fields: ['id']}), [ - {id:3}, - {id:2}, - {id:4}, - {id:1} + {id:3}, + {id:2}, + {id:4}, + {id:1} ]); assert.deepEqual(data.get({ - order: 'age', - filter: function (item) { - return item.group == 2; - }, - fields: ['id'] + order: 'age', + filter: function (item) { + return item.group == 2; + }, + fields: ['id'] }), [ - {id:3}, - {id:1} + {id:3}, + {id:1} ]); assert.deepEqual(data.getIds({ - order: 'age', - filter: function (item) { - return (item.group == 2); - } + order: 'age', + filter: function (item) { + return (item.group == 2); + } }), [3,1]); diff --git a/test/dataview.js b/test/dataview.js index c6769c84..412caa5f 100644 --- a/test/dataview.js +++ b/test/dataview.js @@ -9,42 +9,42 @@ var groups = new DataSet(); // add items with different groups groups.add([ - {id: 1, content: 'Item 1', group: 1}, - {id: 2, content: 'Item 2', group: 2}, - {id: 3, content: 'Item 3', group: 2}, - {id: 4, content: 'Item 4', group: 1}, - {id: 5, content: 'Item 5', group: 3} + {id: 1, content: 'Item 1', group: 1}, + {id: 2, content: 'Item 2', group: 2}, + {id: 3, content: 'Item 3', group: 2}, + {id: 4, content: 'Item 4', group: 1}, + {id: 5, content: 'Item 5', group: 3} ]); var group2 = new DataView(groups, { - filter: function (item) { - return item.group == 2; - } + filter: function (item) { + return item.group == 2; + } }); // test getting the filtered data assert.deepEqual(group2.get(), [ - {id: 2, content: 'Item 2', group: 2}, - {id: 3, content: 'Item 3', group: 2} + {id: 2, content: 'Item 2', group: 2}, + {id: 3, content: 'Item 3', group: 2} ]); // test filtering the view contents assert.deepEqual(group2.get({ - filter: function (item) { - return item.id > 2; - } + filter: function (item) { + return item.id > 2; + } }), [ - {id: 3, content: 'Item 3', group: 2} + {id: 3, content: 'Item 3', group: 2} ]); // test event subscription var groupsTriggerCount = 0; groups.subscribe('*', function () { - groupsTriggerCount++; + groupsTriggerCount++; }); var group2TriggerCount = 0; group2.subscribe('*', function () { - group2TriggerCount++; + group2TriggerCount++; }); groups.update({id:2, content: 'Item 2 (changed)'}); diff --git a/test/dotparser.js b/test/dotparser.js index 93c74214..5fccb7e9 100644 --- a/test/dotparser.js +++ b/test/dotparser.js @@ -3,177 +3,177 @@ var assert = require('assert'), dot = require('../src/graph/dotparser.js'); fs.readFile('test/dot.txt', function (err, data) { - data = String(data); + data = String(data); - var graph = dot.parseDOT(data); + var graph = dot.parseDOT(data); - assert.deepEqual(graph, { - "type": "digraph", - "id": "test_graph", - "rankdir": "LR", - "size": "8,5", - "font": "arial", - "nodes": [ - { - "id": "node1", - "attr": { - "shape": "doublecircle" - } - }, - { - "id": "node2", - "attr": { - "shape": "doublecircle" - } - }, - { - "id": "node3", - "attr": { - "shape": "doublecircle" - } - }, - { - "id": "node4", - "attr": { - "shape": "diamond", - "color": "red" - } - }, - { - "id": "node5", - "attr": { - "shape": "square", - "color": "blue", - "width": 3 - } - }, - { - "id": 6, - "attr": { - "shape": "circle" - } - }, - { - "id": "A", - "attr": { - "shape": "circle" - } - }, - { - "id": "B", - "attr": { - "shape": "circle" - } + assert.deepEqual(graph, { + "type": "digraph", + "id": "test_graph", + "rankdir": "LR", + "size": "8,5", + "font": "arial", + "nodes": [ + { + "id": "node1", + "attr": { + "shape": "doublecircle" + } + }, + { + "id": "node2", + "attr": { + "shape": "doublecircle" + } + }, + { + "id": "node3", + "attr": { + "shape": "doublecircle" + } + }, + { + "id": "node4", + "attr": { + "shape": "diamond", + "color": "red" + } + }, + { + "id": "node5", + "attr": { + "shape": "square", + "color": "blue", + "width": 3 + } + }, + { + "id": 6, + "attr": { + "shape": "circle" + } + }, + { + "id": "A", + "attr": { + "shape": "circle" + } + }, + { + "id": "B", + "attr": { + "shape": "circle" + } + }, + { + "id": "C", + "attr": { + "shape": "circle" + } + } + ], + "edges": [ + { + "from": "node1", + "to": "node1", + "type": "->", + "attr": { + "length": 170, + "fontSize": 12, + "label": "a" + } + }, + { + "from": "node2", + "to": "node3", + "type": "->", + "attr": { + "length": 170, + "fontSize": 12, + "label": "b" + } + }, + { + "from": "node1", + "to": "node4", + "type": "--", + "attr": { + "length": 170, + "fontSize": 12, + "label": "c" + } + }, + { + "from": "node3", + "to": "node4", + "type": "->", + "attr": { + "length": 170, + "fontSize": 12, + "label": "d" + } + }, + { + "from": "node4", + "to": "node5", + "type": "->", + "attr": { + "length": 170, + "fontSize": 12 + } + }, + { + "from": "node5", + "to": 6, + "type": "->", + "attr": { + "length": 170, + "fontSize": 12 + } + }, + { + "from": "A", + "to": { + "nodes": [ + { + "id": "B", + "attr": { + "shape": "circle" + } }, { - "id": "C", - "attr": { - "shape": "circle" - } + "id": "C", + "attr": { + "shape": "circle" + } } - ], - "edges": [ - { - "from": "node1", - "to": "node1", - "type": "->", - "attr": { - "length": 170, - "fontSize": 12, - "label": "a" - } - }, - { - "from": "node2", - "to": "node3", - "type": "->", - "attr": { - "length": 170, - "fontSize": 12, - "label": "b" - } - }, - { - "from": "node1", - "to": "node4", - "type": "--", - "attr": { - "length": 170, - "fontSize": 12, - "label": "c" - } - }, - { - "from": "node3", - "to": "node4", - "type": "->", - "attr": { - "length": 170, - "fontSize": 12, - "label": "d" - } - }, - { - "from": "node4", - "to": "node5", - "type": "->", - "attr": { - "length": 170, - "fontSize": 12 - } - }, - { - "from": "node5", - "to": 6, - "type": "->", - "attr": { - "length": 170, - "fontSize": 12 - } - }, - { - "from": "A", - "to": { - "nodes": [ - { - "id": "B", - "attr": { - "shape": "circle" - } - }, - { - "id": "C", - "attr": { - "shape": "circle" - } - } - ] - }, - "type": "->", - "attr": { - "length": 170, - "fontSize": 12 - } + ] + }, + "type": "->", + "attr": { + "length": 170, + "fontSize": 12 + } + } + ], + "subgraphs": [ + { + "nodes": [ + { + "id": "B", + "attr": { + "shape": "circle" } - ], - "subgraphs": [ - { - "nodes": [ - { - "id": "B", - "attr": { - "shape": "circle" - } - }, - { - "id": "C", - "attr": { - "shape": "circle" - } - } - ] + }, + { + "id": "C", + "attr": { + "shape": "circle" } + } ] - }); + } + ] + }); }); diff --git a/test/eventbus.js b/test/eventbus.js index f213500b..df4d8a94 100644 --- a/test/eventbus.js +++ b/test/eventbus.js @@ -9,21 +9,21 @@ var received = []; var id1 = '1'; bus.on('message', function (event, data, source) { - received.push({ - event: event, - data: data, - source: source - }); + received.push({ + event: event, + data: data, + source: source + }); }, id1); var id2 = '2'; bus.emit('message', {text: 'hello world'}, id2); bus.on('chat:*', function (event, data, source) { - received.push({ - event: event, - data: data, - source: source - }); + received.push({ + event: event, + data: data, + source: source + }); }); bus.emit('chat:1', null, id2); @@ -31,8 +31,8 @@ bus.emit('chat:2', {text: 'hello world'}, id1); // verify if the messages are received assert.deepEqual(received, [ - {event: 'message', data: {text: 'hello world'}, source: id2}, - {event: 'chat:1', data: null, source: id2}, - {event: 'chat:2', data: {text: 'hello world'}, source: id1} + {event: 'message', data: {text: 'hello world'}, source: id2}, + {event: 'chat:1', data: null, source: id2}, + {event: 'chat:2', data: {text: 'hello world'}, source: id1} ]); diff --git a/test/timeline.html b/test/timeline.html index 96023a3a..471e41f7 100644 --- a/test/timeline.html +++ b/test/timeline.html @@ -1,82 +1,82 @@ - - - + + + - + #visualization .itemset { + /*background: rgba(255, 255, 0, 0.5);*/ + } + -
- - -
- -
+
+ + +
+ +
-
+
- + \ No newline at end of file diff --git a/test/timeline_groups.html b/test/timeline_groups.html index b9ed6abb..a8a95a10 100644 --- a/test/timeline_groups.html +++ b/test/timeline_groups.html @@ -1,85 +1,85 @@ - Timeline | Group example + Timeline | Group example - + #visualization { + box-sizing: border-box; + width: 100%; + height: 300px; + } + - - + + - +
- - + +

diff --git a/test/timestep.html b/test/timestep.html index 2ed8b5de..473691c0 100644 --- a/test/timestep.html +++ b/test/timestep.html @@ -1,32 +1,32 @@ - - + + - + \ No newline at end of file diff --git a/tools/watch.js b/tools/watch.js index 6b18f529..79c99a9c 100644 --- a/tools/watch.js +++ b/tools/watch.js @@ -15,17 +15,17 @@ var BUILD_COMMAND = 'jake build'; // rebuilt vis.js on change of code function rebuild() { - var start = +new Date(); - child_process.exec(BUILD_COMMAND, function () { - var end = +new Date(); - console.log('rebuilt in ' + (end - start) + ' ms'); - }); + var start = +new Date(); + child_process.exec(BUILD_COMMAND, function () { + var end = +new Date(); + console.log('rebuilt in ' + (end - start) + ' ms'); + }); } // watch for changes in the code, rebuilt vis.js automatically watch(WATCH_FOLDER, function(filename) { - console.log(filename + ' changed'); - rebuild(); + console.log(filename + ' changed'); + rebuild(); }); rebuild(); diff --git a/vis.js b/vis.js index c1dd372e..08ac4818 100644 --- a/vis.js +++ b/vis.js @@ -5,7 +5,7 @@ * A dynamic, browser-based visualization library. * * @version 0.3.0-SNAPSHOT - * @date 2013-10-30 + * @date 2014-01-03 * * @license * Copyright (C) 2011-2013 Almende B.V, http://almende.com @@ -22,7 +22,7 @@ * License for the specific language governing permissions and limitations under * the License. */ -!function(e){"object"==typeof exports?module.exports=e():"function"==typeof define&&define.amd?define(e):"undefined"!=typeof window?window.vis=e():"undefined"!=typeof global?global.vis=e():"undefined"!=typeof self&&(self.vis=e())}(function(){var define,module,exports; +(function(e){if("function"==typeof bootstrap)bootstrap("vis",e);else if("object"==typeof exports)module.exports=e();else if("function"==typeof define&&define.amd)define(e);else if("undefined"!=typeof ses){if(!ses.ok())return;ses.makeVis=e}else"undefined"!=typeof window?window.vis=e():global.vis=e()})(function(){var define,ses,bootstrap,module,exports; return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= 0) { - value = Math.floor(coercedNumber); - } else { - value = Math.ceil(coercedNumber); - } - } - - return value; - } - - function daysInMonth(year, month) { - return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); - } - - function daysInYear(year) { - return isLeapYear(year) ? 366 : 365; - } - - function isLeapYear(year) { - return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; - } - - function checkOverflow(m) { - var overflow; - if (m._a && m._pf.overflow === -2) { - overflow = - m._a[MONTH] < 0 || m._a[MONTH] > 11 ? MONTH : - m._a[DATE] < 1 || m._a[DATE] > daysInMonth(m._a[YEAR], m._a[MONTH]) ? DATE : - m._a[HOUR] < 0 || m._a[HOUR] > 23 ? HOUR : - m._a[MINUTE] < 0 || m._a[MINUTE] > 59 ? MINUTE : - m._a[SECOND] < 0 || m._a[SECOND] > 59 ? SECOND : - m._a[MILLISECOND] < 0 || m._a[MILLISECOND] > 999 ? MILLISECOND : - -1; - - if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) { - overflow = DATE; - } - - m._pf.overflow = overflow; - } - } - - function initializeParsingFlags(config) { - config._pf = { - empty : false, - unusedTokens : [], - unusedInput : [], - overflow : -2, - charsLeftOver : 0, - nullInput : false, - invalidMonth : null, - invalidFormat : false, - userInvalidated : false, - iso: false - }; - } - - function isValid(m) { - if (m._isValid == null) { - m._isValid = !isNaN(m._d.getTime()) && - m._pf.overflow < 0 && - !m._pf.empty && - !m._pf.invalidMonth && - !m._pf.nullInput && - !m._pf.invalidFormat && - !m._pf.userInvalidated; - - if (m._strict) { - m._isValid = m._isValid && - m._pf.charsLeftOver === 0 && - m._pf.unusedTokens.length === 0; - } - } - return m._isValid; + return units ? unitAliases[units] || units.toLowerCase().replace(/(.)s$/, '$1') : units; } - function normalizeLanguage(key) { - return key ? key.toLowerCase().replace('_', '-') : key; - } /************************************ Languages @@ -2194,15 +1988,9 @@ else { week : function (mom) { return weekOfYear(mom, this._week.dow, this._week.doy).week; }, - _week : { dow : 0, // Sunday is the first day of the week. doy : 6 // The week that contains Jan 1st is the first week of the year. - }, - - _invalidDate: 'Invalid date', - invalidDate: function () { - return this._invalidDate; } }); @@ -2231,60 +2019,28 @@ else { // definition for 'en', so long as 'en' has already been loaded using // moment.lang. function getLangDefinition(key) { - var i = 0, j, lang, next, split, - get = function (k) { - if (!languages[k] && hasModule) { - try { - require('./lang/' + k); - } catch (e) { } - } - return languages[k]; - }; - if (!key) { return moment.fn._lang; } - - if (!isArray(key)) { - //short-circuit everything else - lang = get(key); - if (lang) { - return lang; - } - key = [key]; - } - - //pick the language from the array - //try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each - //substring from most specific to least, but move to the next array item if it's a more specific variant than the current root - while (i < key.length) { - split = normalizeLanguage(key[i]).split('-'); - j = split.length; - next = normalizeLanguage(key[i + 1]); - next = next ? next.split('-') : null; - while (j > 0) { - lang = get(split.slice(0, j).join('-')); - if (lang) { - return lang; - } - if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) { - //the next array item is better than a shallower substring of this one - break; - } - j--; + if (!languages[key] && hasModule) { + try { + require('./lang/' + key); + } catch (e) { + // call with no params to set to default + return moment.fn._lang; } - i++; } - return moment.fn._lang; + return languages[key] || moment.fn._lang; } + /************************************ Formatting ************************************/ function removeFormattingTokens(input) { - if (input.match(/\[[\s\S]/)) { + if (input.match(/\[.*\]/)) { return input.replace(/^\[|\]$/g, ""); } return input.replace(/\\/g, ""); @@ -2313,10 +2069,6 @@ else { // format date using native date object function formatMoment(m, format) { - if (!m.isValid()) { - return m.lang().invalidDate(); - } - format = expandFormat(format, m.lang()); if (!formatFunctions[format]) { @@ -2333,11 +2085,9 @@ else { return lang.longDateFormat(input) || input; } - localFormattingTokens.lastIndex = 0; - while (i >= 0 && localFormattingTokens.test(format)) { + while (i-- && (localFormattingTokens.lastIndex = 0, + localFormattingTokens.test(format))) { format = format.replace(localFormattingTokens, replaceLongDateFormatTokens); - localFormattingTokens.lastIndex = 0; - i -= 1; } return format; @@ -2351,17 +2101,12 @@ else { // get the regex to find the next token function getParseRegexForToken(token, config) { - var a; switch (token) { case 'DDDD': return parseTokenThreeDigits; case 'YYYY': - case 'GGGG': - case 'gggg': return parseTokenFourDigits; case 'YYYYY': - case 'GGGGG': - case 'ggggg': return parseTokenSixDigits; case 'S': case 'SS': @@ -2384,13 +2129,9 @@ else { return parseTokenTimezone; case 'T': return parseTokenT; - case 'SSSS': - return parseTokenDigits; case 'MM': case 'DD': case 'YY': - case 'GG': - case 'gg': case 'HH': case 'hh': case 'mm': @@ -2402,23 +2143,16 @@ else { case 'h': case 'm': case 's': - case 'w': - case 'ww': - case 'W': - case 'WW': - case 'e': - case 'E': return parseTokenOneOrTwoDigits; default : - a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), "i")); - return a; + return new RegExp(token.replace('\\', '')); } } function timezoneMinutesFromString(string) { var tzchunk = (parseTokenTimezone.exec(string) || [])[0], parts = (tzchunk + '').match(parseTimezoneChunker) || ['-', 0, 0], - minutes = +(parts[1] * 60) + toInt(parts[2]); + minutes = +(parts[1] * 60) + ~~parts[2]; return parts[0] === '+' ? -minutes : minutes; } @@ -2432,7 +2166,7 @@ else { case 'M' : // fall through to MM case 'MM' : if (input != null) { - datePartArray[MONTH] = toInt(input) - 1; + datePartArray[1] = ~~input - 1; } break; case 'MMM' : // fall through to MMMM @@ -2440,33 +2174,33 @@ else { a = getLangDefinition(config._l).monthsParse(input); // if we didn't find a month name, mark the date as invalid. if (a != null) { - datePartArray[MONTH] = a; + datePartArray[1] = a; } else { - config._pf.invalidMonth = input; + config._isValid = false; } break; // DAY OF MONTH case 'D' : // fall through to DD case 'DD' : if (input != null) { - datePartArray[DATE] = toInt(input); + datePartArray[2] = ~~input; } break; // DAY OF YEAR case 'DDD' : // fall through to DDDD case 'DDDD' : if (input != null) { - config._dayOfYear = toInt(input); + datePartArray[1] = 0; + datePartArray[2] = ~~input; } - break; // YEAR case 'YY' : - datePartArray[YEAR] = toInt(input) + (toInt(input) > 68 ? 1900 : 2000); + datePartArray[0] = ~~input + (~~input > 68 ? 1900 : 2000); break; case 'YYYY' : case 'YYYYY' : - datePartArray[YEAR] = toInt(input); + datePartArray[0] = ~~input; break; // AM / PM case 'a' : // fall through to A @@ -2478,24 +2212,23 @@ else { case 'HH' : // fall through to hh case 'h' : // fall through to hh case 'hh' : - datePartArray[HOUR] = toInt(input); + datePartArray[3] = ~~input; break; // MINUTE case 'm' : // fall through to mm case 'mm' : - datePartArray[MINUTE] = toInt(input); + datePartArray[4] = ~~input; break; // SECOND case 's' : // fall through to ss case 'ss' : - datePartArray[SECOND] = toInt(input); + datePartArray[5] = ~~input; break; // MILLISECOND case 'S' : case 'SS' : case 'SSS' : - case 'SSSS' : - datePartArray[MILLISECOND] = toInt(('0.' + input) * 1000); + datePartArray[6] = ~~ (('0.' + input) * 1000); break; // UNIX TIMESTAMP WITH MS case 'X': @@ -2507,29 +2240,11 @@ else { config._useUTC = true; config._tzm = timezoneMinutesFromString(input); break; - case 'w': - case 'ww': - case 'W': - case 'WW': - case 'd': - case 'dd': - case 'ddd': - case 'dddd': - case 'e': - case 'E': - token = token.substr(0, 1); - /* falls through */ - case 'gg': - case 'gggg': - case 'GG': - case 'GGGG': - case 'GGGGG': - token = token.substr(0, 2); - if (input) { - config._w = config._w || {}; - config._w[token] = input; - } - break; + } + + // if the input is null, the date is not valid + if (input == null) { + config._isValid = false; } } @@ -2537,65 +2252,19 @@ else { // the array should mirror the parameters below // note: all values past the year are optional and will default to the lowest possible value. // [year, month, day , hour, minute, second, millisecond] - function dateFromConfig(config) { - var i, date, input = [], currentDate, - yearToUse, fixYear, w, temp, lang, weekday, week; + function dateFromArray(config) { + var i, date, input = [], currentDate; if (config._d) { return; } - currentDate = currentDateArray(config); - - //compute day of the year from weeks and weekdays - if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { - fixYear = function (val) { - return val ? - (val.length < 3 ? (parseInt(val, 10) > 68 ? '19' + val : '20' + val) : val) : - (config._a[YEAR] == null ? moment().weekYear() : config._a[YEAR]); - }; - - w = config._w; - if (w.GG != null || w.W != null || w.E != null) { - temp = dayOfYearFromWeeks(fixYear(w.GG), w.W || 1, w.E, 4, 1); - } - else { - lang = getLangDefinition(config._l); - weekday = w.d != null ? parseWeekday(w.d, lang) : - (w.e != null ? parseInt(w.e, 10) + lang._week.dow : 0); - - week = parseInt(w.w, 10) || 1; - - //if we're parsing 'd', then the low day numbers may be next week - if (w.d != null && weekday < lang._week.dow) { - week++; - } - - temp = dayOfYearFromWeeks(fixYear(w.gg), week, weekday, lang._week.doy, lang._week.dow); - } - - config._a[YEAR] = temp.year; - config._dayOfYear = temp.dayOfYear; - } - - //if the day of the year is set, figure out what it is - if (config._dayOfYear) { - yearToUse = config._a[YEAR] == null ? currentDate[YEAR] : config._a[YEAR]; - - if (config._dayOfYear > daysInYear(yearToUse)) { - config._pf._overflowDayOfYear = true; - } - - date = makeUTCDate(yearToUse, 0, config._dayOfYear); - config._a[MONTH] = date.getUTCMonth(); - config._a[DATE] = date.getUTCDate(); - } - // Default to current date. // * if no year, month, day of month are given, default to today // * if day of month is given, default month and year // * if month is given, default only year // * if year is given, don't default anything + currentDate = currentDateArray(config); for (i = 0; i < 3 && config._a[i] == null; ++i) { config._a[i] = input[i] = currentDate[i]; } @@ -2606,31 +2275,40 @@ else { } // add the offsets to the time to be parsed so that we can have a clean array for checking isValid - input[HOUR] += toInt((config._tzm || 0) / 60); - input[MINUTE] += toInt((config._tzm || 0) % 60); + input[3] += ~~((config._tzm || 0) / 60); + input[4] += ~~((config._tzm || 0) % 60); + + date = new Date(0); + + if (config._useUTC) { + date.setUTCFullYear(input[0], input[1], input[2]); + date.setUTCHours(input[3], input[4], input[5], input[6]); + } else { + date.setFullYear(input[0], input[1], input[2]); + date.setHours(input[3], input[4], input[5], input[6]); + } - config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input); + config._d = date; } function dateFromObject(config) { - var normalizedInput; + var o = config._i; if (config._d) { return; } - normalizedInput = normalizeObjectUnits(config._i); config._a = [ - normalizedInput.year, - normalizedInput.month, - normalizedInput.day, - normalizedInput.hour, - normalizedInput.minute, - normalizedInput.second, - normalizedInput.millisecond + o.years || o.year || o.y, + o.months || o.month || o.M, + o.days || o.day || o.d, + o.hours || o.hour || o.h, + o.minutes || o.minute || o.m, + o.seconds || o.second || o.s, + o.milliseconds || o.millisecond || o.ms ]; - dateFromConfig(config); + dateFromArray(config); } function currentDateArray(config) { @@ -2648,116 +2326,74 @@ else { // date from string and format string function makeDateFromStringAndFormat(config) { - - config._a = []; - config._pf.empty = true; - // This array is used to make a Date, either with `new Date` or `Date.UTC` var lang = getLangDefinition(config._l), string = '' + config._i, - i, parsedInput, tokens, token, skipped, - stringLength = string.length, - totalParsedInputLength = 0; + i, parsedInput, tokens; + + tokens = expandFormat(config._f, lang).match(formattingTokens); - tokens = expandFormat(config._f, lang).match(formattingTokens) || []; + config._a = []; for (i = 0; i < tokens.length; i++) { - token = tokens[i]; - parsedInput = (getParseRegexForToken(token, config).exec(string) || [])[0]; + parsedInput = (getParseRegexForToken(tokens[i], config).exec(string) || [])[0]; if (parsedInput) { - skipped = string.substr(0, string.indexOf(parsedInput)); - if (skipped.length > 0) { - config._pf.unusedInput.push(skipped); - } string = string.slice(string.indexOf(parsedInput) + parsedInput.length); - totalParsedInputLength += parsedInput.length; } - // don't parse if it's not a known token - if (formatTokenFunctions[token]) { - if (parsedInput) { - config._pf.empty = false; - } - else { - config._pf.unusedTokens.push(token); - } - addTimeToArrayFromToken(token, parsedInput, config); - } - else if (config._strict && !parsedInput) { - config._pf.unusedTokens.push(token); + // don't parse if its not a known token + if (formatTokenFunctions[tokens[i]]) { + addTimeToArrayFromToken(tokens[i], parsedInput, config); } } - // add remaining unparsed input length to the string - config._pf.charsLeftOver = stringLength - totalParsedInputLength; - if (string.length > 0) { - config._pf.unusedInput.push(string); + // add remaining unparsed input to the string + if (string) { + config._il = string; } // handle am pm - if (config._isPm && config._a[HOUR] < 12) { - config._a[HOUR] += 12; + if (config._isPm && config._a[3] < 12) { + config._a[3] += 12; } // if is 12 am, change hours to 0 - if (config._isPm === false && config._a[HOUR] === 12) { - config._a[HOUR] = 0; + if (config._isPm === false && config._a[3] === 12) { + config._a[3] = 0; } - - dateFromConfig(config); - checkOverflow(config); - } - - function unescapeFormat(s) { - return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) { - return p1 || p2 || p3 || p4; - }); - } - - // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript - function regexpEscape(s) { - return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + // return + dateFromArray(config); } // date from string and array of format strings function makeDateFromStringAndArray(config) { var tempConfig, + tempMoment, bestMoment, - scoreToBeat, + scoreToBeat = 99, i, currentScore; - if (config._f.length === 0) { - config._pf.invalidFormat = true; - config._d = new Date(NaN); - return; - } - for (i = 0; i < config._f.length; i++) { - currentScore = 0; tempConfig = extend({}, config); - initializeParsingFlags(tempConfig); tempConfig._f = config._f[i]; makeDateFromStringAndFormat(tempConfig); + tempMoment = new Moment(tempConfig); - if (!isValid(tempConfig)) { - continue; - } - - // if there is any input that was not parsed add a penalty for that format - currentScore += tempConfig._pf.charsLeftOver; - - //or tokens - currentScore += tempConfig._pf.unusedTokens.length * 10; + currentScore = compareArrays(tempConfig._a, tempMoment.toArray()); - tempConfig._pf.score = currentScore; + // if there is any input that was not parsed + // add a penalty for that format + if (tempMoment._il) { + currentScore += tempMoment._il.length; + } - if (scoreToBeat == null || currentScore < scoreToBeat) { + if (currentScore < scoreToBeat) { scoreToBeat = currentScore; - bestMoment = tempConfig; + bestMoment = tempMoment; } } - extend(config, bestMoment || tempConfig); + extend(config, bestMoment); } // date from iso format @@ -2767,14 +2403,8 @@ else { match = isoRegex.exec(string); if (match) { - config._pf.iso = true; - for (i = 4; i > 0; i--) { - if (match[i]) { - // match[5] should be "T" or undefined - config._f = isoDates[i - 1] + (match[6] || " "); - break; - } - } + // match[2] should be "T" or undefined + config._f = 'YYYY-MM-DD' + (match[2] || " "); for (i = 0; i < 4; i++) { if (isoTimes[i][1].exec(string)) { config._f += isoTimes[i][0]; @@ -2782,11 +2412,10 @@ else { } } if (parseTokenTimezone.exec(string)) { - config._f += "Z"; + config._f += " Z"; } makeDateFromStringAndFormat(config); - } - else { + } else { config._d = new Date(string); } } @@ -2803,8 +2432,8 @@ else { makeDateFromString(config); } else if (isArray(input)) { config._a = input.slice(0); - dateFromConfig(config); - } else if (isDate(input)) { + dateFromArray(config); + } else if (input instanceof Date) { config._d = new Date(+input); } else if (typeof(input) === 'object') { dateFromObject(config); @@ -2813,40 +2442,6 @@ else { } } - function makeDate(y, m, d, h, M, s, ms) { - //can't just apply() to create a date: - //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply - var date = new Date(y, m, d, h, M, s, ms); - - //the date constructor doesn't accept years < 1970 - if (y < 1970) { - date.setFullYear(y); - } - return date; - } - - function makeUTCDate(y) { - var date = new Date(Date.UTC.apply(null, arguments)); - if (y < 1970) { - date.setUTCFullYear(y); - } - return date; - } - - function parseWeekday(input, language) { - if (typeof input === 'string') { - if (!isNaN(input)) { - input = parseInt(input, 10); - } - else { - input = language.weekdaysParse(input); - if (typeof input !== 'number') { - return null; - } - } - } - return input; - } /************************************ Relative Time @@ -2914,20 +2509,6 @@ else { }; } - //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday - function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) { - var d = new Date(Date.UTC(year, 0)).getUTCDay(), - daysToAdd, dayOfYear; - - weekday = weekday != null ? weekday : firstDayOfWeek; - daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0); - dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1; - - return { - year: dayOfYear > 0 ? year : year - 1, - dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear - }; - } /************************************ Top Level Functions @@ -2937,12 +2518,8 @@ else { var input = config._i, format = config._f; - if (typeof config._pf === 'undefined') { - initializeParsingFlags(config); - } - - if (input === null) { - return moment.invalid({nullInput: true}); + if (input === null || input === '') { + return null; } if (typeof input === 'string') { @@ -2951,7 +2528,6 @@ else { if (moment.isMoment(input)) { config = extend({}, input); - config._d = new Date(+input._d); } else if (format) { if (isArray(format)) { @@ -2966,38 +2542,24 @@ else { return new Moment(config); } - moment = function (input, format, lang, strict) { - if (typeof(lang) === "boolean") { - strict = lang; - lang = undefined; - } + moment = function (input, format, lang) { return makeMoment({ _i : input, _f : format, _l : lang, - _strict : strict, _isUTC : false }); }; // creating with utc - moment.utc = function (input, format, lang, strict) { - var m; - - if (typeof(lang) === "boolean") { - strict = lang; - lang = undefined; - } - m = makeMoment({ + moment.utc = function (input, format, lang) { + return makeMoment({ _useUTC : true, _isUTC : true, _l : lang, _i : input, - _f : format, - _strict : strict + _f : format }).utc(); - - return m; }; // creating with unix timestamp (in seconds) @@ -3010,13 +2572,9 @@ else { var isDuration = moment.isDuration(input), isNumber = (typeof input === 'number'), duration = (isDuration ? input._input : (isNumber ? {} : input)), - // matching against regexp is expensive, do it on demand - match = null, + matched = aspNetTimeSpanJsonRegex.exec(input), sign, - ret, - parseIso, - timeEmpty, - dateTimeEmpty; + ret; if (isNumber) { if (key) { @@ -3024,34 +2582,15 @@ else { } else { duration.milliseconds = input; } - } else if (!!(match = aspNetTimeSpanJsonRegex.exec(input))) { - sign = (match[1] === "-") ? -1 : 1; + } else if (matched) { + sign = (matched[1] === "-") ? -1 : 1; duration = { y: 0, - d: toInt(match[DATE]) * sign, - h: toInt(match[HOUR]) * sign, - m: toInt(match[MINUTE]) * sign, - s: toInt(match[SECOND]) * sign, - ms: toInt(match[MILLISECOND]) * sign - }; - } else if (!!(match = isoDurationRegex.exec(input))) { - sign = (match[1] === "-") ? -1 : 1; - parseIso = function (inp) { - // We'd normally use ~~inp for this, but unfortunately it also - // converts floats to ints. - // inp may be undefined, so careful calling replace on it. - var res = inp && parseFloat(inp.replace(',', '.')); - // apply sign while we're at it - return (isNaN(res) ? 0 : res) * sign; - }; - duration = { - y: parseIso(match[2]), - M: parseIso(match[3]), - d: parseIso(match[4]), - h: parseIso(match[5]), - m: parseIso(match[6]), - s: parseIso(match[7]), - w: parseIso(match[8]) + d: ~~matched[2] * sign, + h: ~~matched[3] * sign, + m: ~~matched[4] * sign, + s: ~~matched[5] * sign, + ms: ~~matched[6] * sign }; } @@ -3078,20 +2617,20 @@ else { // no arguments are passed in, it will simply return the current global // language key. moment.lang = function (key, values) { - var r; if (!key) { return moment.fn._lang._abbr; } + key = key.toLowerCase(); + key = key.replace('_', '-'); if (values) { - loadLang(normalizeLanguage(key), values); + loadLang(key, values); } else if (values === null) { unloadLang(key); key = 'en'; } else if (!languages[key]) { getLangDefinition(key); } - r = moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key); - return r._abbr; + moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key); }; // returns language data @@ -3112,29 +2651,6 @@ else { return obj instanceof Duration; }; - for (i = lists.length - 1; i >= 0; --i) { - makeList(lists[i]); - } - - moment.normalizeUnits = function (units) { - return normalizeUnits(units); - }; - - moment.invalid = function (flags) { - var m = moment.utc(NaN); - if (flags != null) { - extend(m._pf, flags); - } - else { - m._pf.userInvalidated = true; - } - - return m; - }; - - moment.parseZone = function (input) { - return moment(input).parseZone(); - }; /************************************ Moment Prototype @@ -3156,7 +2672,7 @@ else { }, toString : function () { - return this.clone().lang('en').format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ"); + return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ"); }, toDate : function () { @@ -3181,24 +2697,22 @@ else { }, isValid : function () { - return isValid(this); - }, - - isDSTShifted : function () { - - if (this._a) { - return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0; + if (this._isValid == null) { + if (this._a) { + this._isValid = !compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()); + } else { + this._isValid = !isNaN(this._d.getTime()); + } } - - return false; - }, - - parsingFlags : function () { - return extend({}, this._pf); + return !!this._isValid; }, invalidAt: function () { - return this._pf.overflow; + var i, arr1 = this._a, arr2 = (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray(); + for (i = 6; i >= 0 && arr1[i] === arr2[i]; --i) { + // empty loop body + } + return i; }, utc : function () { @@ -3294,7 +2808,8 @@ else { }, isLeapYear : function () { - return isLeapYear(this.year()); + var year = this.year(); + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; }, isDST : function () { @@ -3305,7 +2820,12 @@ else { day : function (input) { var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); if (input != null) { - input = parseWeekday(input, this.lang()); + if (typeof input === 'string') { + input = this.lang().weekdaysParse(input); + if (typeof input !== 'number') { + return this; + } + } return this.add({ d : input - day }); } else { return day; @@ -3348,7 +2868,7 @@ else { this.date(1); /* falls through */ case 'week': - case 'isoWeek': + case 'isoweek': case 'day': this.hours(0); /* falls through */ @@ -3366,7 +2886,7 @@ else { // weeks are a special case if (units === 'week') { this.weekday(0); - } else if (units === 'isoWeek') { + } else if (units === 'isoweek') { this.isoWeekday(1); } @@ -3375,7 +2895,7 @@ else { endOf: function (units) { units = normalizeUnits(units); - return this.startOf(units).add((units === 'isoWeek' ? 'week' : units), 1).subtract('ms', 1); + return this.startOf(units).add((units === 'isoweek' ? 'week' : units), 1).subtract('ms', 1); }, isAfter: function (input, units) { @@ -3431,13 +2951,6 @@ else { return this._isUTC ? "Coordinated Universal Time" : ""; }, - parseZone : function () { - if (typeof this._i === 'string') { - this.zone(this._i); - } - return this; - }, - hasAlignedHourOffset : function (input) { if (!input) { input = 0; @@ -3450,7 +2963,7 @@ else { }, daysInMonth : function () { - return daysInMonth(this.year(), this.month()); + return moment.utc([this.year(), this.month() + 1, 0]).date(); }, dayOfYear : function (input) { @@ -3479,7 +2992,7 @@ else { }, weekday : function (input) { - var weekday = (this.day() + 7 - this.lang()._week.dow) % 7; + var weekday = (this._d.getDay() + 7 - this.lang()._week.dow) % 7; return input == null ? weekday : this.add("d", input - weekday); }, @@ -3492,15 +3005,12 @@ else { get : function (units) { units = normalizeUnits(units); - return this[units](); + return this[units.toLowerCase()](); }, set : function (units, value) { units = normalizeUnits(units); - if (typeof this[units] === 'function') { - this[units](value); - } - return this; + this[units.toLowerCase()](value); }, // If passed a language key, it will set the language for this @@ -3592,7 +3102,7 @@ else { return this._milliseconds + this._days * 864e5 + (this._months % 12) * 2592e6 + - toInt(this._months / 12) * 31536e6; + ~~(this._months / 12) * 31536e6; }, humanize : function (withSuffix) { @@ -3641,33 +3151,7 @@ else { return this['as' + units.charAt(0).toUpperCase() + units.slice(1) + 's'](); }, - lang : moment.fn.lang, - - toIsoString : function () { - // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js - var years = Math.abs(this.years()), - months = Math.abs(this.months()), - days = Math.abs(this.days()), - hours = Math.abs(this.hours()), - minutes = Math.abs(this.minutes()), - seconds = Math.abs(this.seconds() + this.milliseconds() / 1000); - - if (!this.asSeconds()) { - // this is the same as C#'s (Noda) and python (isodate)... - // but not other JS (goog.date) - return 'P0D'; - } - - return (this.asSeconds() < 0 ? '-' : '') + - 'P' + - (years ? years + 'Y' : '') + - (months ? months + 'M' : '') + - (days ? days + 'D' : '') + - ((hours || minutes || seconds) ? 'T' : '') + - (hours ? hours + 'H' : '') + - (minutes ? minutes + 'M' : '') + - (seconds ? seconds + 'S' : ''); - } + lang : moment.fn.lang }); function makeDurationGetter(name) { @@ -3704,7 +3188,7 @@ else { moment.lang('en', { ordinal : function (number) { var b = number % 10, - output = (toInt(number % 100 / 10) === 1) ? 'th' : + output = (~~ (number % 100 / 10) === 1) ? 'th' : (b === 1) ? 'st' : (b === 2) ? 'nd' : (b === 3) ? 'rd' : 'th'; @@ -3718,46 +3202,23 @@ else { Exposing Moment ************************************/ - function makeGlobal(deprecate) { - var warned = false, local_moment = moment; - /*global ender:false */ - if (typeof ender !== 'undefined') { - return; - } - // here, `this` means `window` in the browser, or `global` on the server - // add `moment` as a global object via a string identifier, - // for Closure Compiler "advanced" mode - if (deprecate) { - this.moment = function () { - if (!warned && console && console.warn) { - warned = true; - console.warn( - "Accessing Moment through the global scope is " + - "deprecated, and will be removed in an upcoming " + - "release."); - } - return local_moment.apply(null, arguments); - }; - } else { - this['moment'] = moment; - } - } // CommonJS module is defined if (hasModule) { module.exports = moment; - makeGlobal(true); - } else if (typeof define === "function" && define.amd) { - define("moment", function (require, exports, module) { - if (module.config().noGlobal !== true) { - // If user provided noGlobal, he is aware of global - makeGlobal(module.config().noGlobal === undefined); - } - + } + /*global ender:false */ + if (typeof ender === 'undefined') { + // here, `this` means `window` in the browser, or `global` on the server + // add `moment` as a global object via a string identifier, + // for Closure Compiler "advanced" mode + this['moment'] = moment; + } + /*global define:false */ + if (typeof define === "function" && define.amd) { + define("moment", [], function () { return moment; }); - } else { - makeGlobal(); } }).call(this); @@ -3786,31 +3247,31 @@ else { // it here in that case. // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/ if(!Array.prototype.indexOf) { - Array.prototype.indexOf = function(obj){ - for(var i = 0; i < this.length; i++){ - if(this[i] == obj){ - return i; - } - } - return -1; - }; - - try { - console.log("Warning: Ancient browser detected. Please update your browser"); - } - catch (err) { + Array.prototype.indexOf = function(obj){ + for(var i = 0; i < this.length; i++){ + if(this[i] == obj){ + return i; + } } + return -1; + }; + + try { + console.log("Warning: Ancient browser detected. Please update your browser"); + } + catch (err) { + } } // Internet Explorer 8 and older does not support Array.forEach, so we define // it here in that case. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach if (!Array.prototype.forEach) { - Array.prototype.forEach = function(fn, scope) { - for(var i = 0, len = this.length; i < len; ++i) { - fn.call(scope || this, this[i], i, this); - } + Array.prototype.forEach = function(fn, scope) { + for(var i = 0, len = this.length; i < len; ++i) { + fn.call(scope || this, this[i], i, this); } + } } // Internet Explorer 8 and older does not support Array.map, so we define it @@ -3819,106 +3280,106 @@ if (!Array.prototype.forEach) { // Production steps of ECMA-262, Edition 5, 15.4.4.19 // Reference: http://es5.github.com/#x15.4.4.19 if (!Array.prototype.map) { - Array.prototype.map = function(callback, thisArg) { - - var T, A, k; + Array.prototype.map = function(callback, thisArg) { - if (this == null) { - throw new TypeError(" this is null or not defined"); - } + var T, A, k; - // 1. Let O be the result of calling ToObject passing the |this| value as the argument. - var O = Object(this); + if (this == null) { + throw new TypeError(" this is null or not defined"); + } - // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length". - // 3. Let len be ToUint32(lenValue). - var len = O.length >>> 0; + // 1. Let O be the result of calling ToObject passing the |this| value as the argument. + var O = Object(this); - // 4. If IsCallable(callback) is false, throw a TypeError exception. - // See: http://es5.github.com/#x9.11 - if (typeof callback !== "function") { - throw new TypeError(callback + " is not a function"); - } + // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length". + // 3. Let len be ToUint32(lenValue). + var len = O.length >>> 0; - // 5. If thisArg was supplied, let T be thisArg; else let T be undefined. - if (thisArg) { - T = thisArg; - } + // 4. If IsCallable(callback) is false, throw a TypeError exception. + // See: http://es5.github.com/#x9.11 + if (typeof callback !== "function") { + throw new TypeError(callback + " is not a function"); + } - // 6. Let A be a new array created as if by the expression new Array(len) where Array is - // the standard built-in constructor with that name and len is the value of len. - A = new Array(len); + // 5. If thisArg was supplied, let T be thisArg; else let T be undefined. + if (thisArg) { + T = thisArg; + } - // 7. Let k be 0 - k = 0; + // 6. Let A be a new array created as if by the expression new Array(len) where Array is + // the standard built-in constructor with that name and len is the value of len. + A = new Array(len); - // 8. Repeat, while k < len - while(k < len) { + // 7. Let k be 0 + k = 0; - var kValue, mappedValue; + // 8. Repeat, while k < len + while(k < len) { - // a. Let Pk be ToString(k). - // This is implicit for LHS operands of the in operator - // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk. - // This step can be combined with c - // c. If kPresent is true, then - if (k in O) { + var kValue, mappedValue; - // i. Let kValue be the result of calling the Get internal method of O with argument Pk. - kValue = O[ k ]; + // a. Let Pk be ToString(k). + // This is implicit for LHS operands of the in operator + // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk. + // This step can be combined with c + // c. If kPresent is true, then + if (k in O) { - // ii. Let mappedValue be the result of calling the Call internal method of callback - // with T as the this value and argument list containing kValue, k, and O. - mappedValue = callback.call(T, kValue, k, O); + // i. Let kValue be the result of calling the Get internal method of O with argument Pk. + kValue = O[ k ]; - // iii. Call the DefineOwnProperty internal method of A with arguments - // Pk, Property Descriptor {Value: mappedValue, : true, Enumerable: true, Configurable: true}, - // and false. + // ii. Let mappedValue be the result of calling the Call internal method of callback + // with T as the this value and argument list containing kValue, k, and O. + mappedValue = callback.call(T, kValue, k, O); - // In browsers that support Object.defineProperty, use the following: - // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true }); + // iii. Call the DefineOwnProperty internal method of A with arguments + // Pk, Property Descriptor {Value: mappedValue, : true, Enumerable: true, Configurable: true}, + // and false. - // For best browser support, use the following: - A[ k ] = mappedValue; - } - // d. Increase k by 1. - k++; - } + // In browsers that support Object.defineProperty, use the following: + // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true }); - // 9. return A - return A; - }; + // For best browser support, use the following: + A[ k ] = mappedValue; + } + // d. Increase k by 1. + k++; + } + + // 9. return A + return A; + }; } // Internet Explorer 8 and older does not support Array.filter, so we define it // here in that case. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/filter if (!Array.prototype.filter) { - Array.prototype.filter = function(fun /*, thisp */) { - "use strict"; + Array.prototype.filter = function(fun /*, thisp */) { + "use strict"; - if (this == null) { - throw new TypeError(); - } + if (this == null) { + throw new TypeError(); + } - var t = Object(this); - var len = t.length >>> 0; - if (typeof fun != "function") { - throw new TypeError(); - } + var t = Object(this); + var len = t.length >>> 0; + if (typeof fun != "function") { + throw new TypeError(); + } - var res = []; - var thisp = arguments[1]; - for (var i = 0; i < len; i++) { - if (i in t) { - var val = t[i]; // in case fun mutates this - if (fun.call(thisp, val, i, t)) - res.push(val); - } - } + var res = []; + var thisp = arguments[1]; + for (var i = 0; i < len; i++) { + if (i in t) { + var val = t[i]; // in case fun mutates this + if (fun.call(thisp, val, i, t)) + res.push(val); + } + } - return res; - }; + return res; + }; } @@ -3926,112 +3387,112 @@ if (!Array.prototype.filter) { // here in that case. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/keys if (!Object.keys) { - Object.keys = (function () { - var hasOwnProperty = Object.prototype.hasOwnProperty, - hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'), - dontEnums = [ - 'toString', - 'toLocaleString', - 'valueOf', - 'hasOwnProperty', - 'isPrototypeOf', - 'propertyIsEnumerable', - 'constructor' - ], - dontEnumsLength = dontEnums.length; - - return function (obj) { - if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) { - throw new TypeError('Object.keys called on non-object'); - } + Object.keys = (function () { + var hasOwnProperty = Object.prototype.hasOwnProperty, + hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'), + dontEnums = [ + 'toString', + 'toLocaleString', + 'valueOf', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'constructor' + ], + dontEnumsLength = dontEnums.length; + + return function (obj) { + if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) { + throw new TypeError('Object.keys called on non-object'); + } - var result = []; + var result = []; - for (var prop in obj) { - if (hasOwnProperty.call(obj, prop)) result.push(prop); - } + for (var prop in obj) { + if (hasOwnProperty.call(obj, prop)) result.push(prop); + } - if (hasDontEnumBug) { - for (var i=0; i < dontEnumsLength; i++) { - if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]); - } - } - return result; + if (hasDontEnumBug) { + for (var i=0; i < dontEnumsLength; i++) { + if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]); } - })() + } + return result; + } + })() } // Internet Explorer 8 and older does not support Array.isArray, // so we define it here in that case. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/isArray if(!Array.isArray) { - Array.isArray = function (vArg) { - return Object.prototype.toString.call(vArg) === "[object Array]"; - }; + Array.isArray = function (vArg) { + return Object.prototype.toString.call(vArg) === "[object Array]"; + }; } // Internet Explorer 8 and older does not support Function.bind, // so we define it here in that case. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind if (!Function.prototype.bind) { - Function.prototype.bind = function (oThis) { - if (typeof this !== "function") { - // closest thing possible to the ECMAScript 5 internal IsCallable function - throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); - } - - var aArgs = Array.prototype.slice.call(arguments, 1), - fToBind = this, - fNOP = function () {}, - fBound = function () { - return fToBind.apply(this instanceof fNOP && oThis - ? this - : oThis, - aArgs.concat(Array.prototype.slice.call(arguments))); - }; + Function.prototype.bind = function (oThis) { + if (typeof this !== "function") { + // closest thing possible to the ECMAScript 5 internal IsCallable function + throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); + } + + var aArgs = Array.prototype.slice.call(arguments, 1), + fToBind = this, + fNOP = function () {}, + fBound = function () { + return fToBind.apply(this instanceof fNOP && oThis + ? this + : oThis, + aArgs.concat(Array.prototype.slice.call(arguments))); + }; - fNOP.prototype = this.prototype; - fBound.prototype = new fNOP(); + fNOP.prototype = this.prototype; + fBound.prototype = new fNOP(); - return fBound; - }; + return fBound; + }; } // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create if (!Object.create) { - Object.create = function (o) { - if (arguments.length > 1) { - throw new Error('Object.create implementation only accepts the first parameter.'); - } - function F() {} - F.prototype = o; - return new F(); - }; + Object.create = function (o) { + if (arguments.length > 1) { + throw new Error('Object.create implementation only accepts the first parameter.'); + } + function F() {} + F.prototype = o; + return new F(); + }; } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind if (!Function.prototype.bind) { - Function.prototype.bind = function (oThis) { - if (typeof this !== "function") { - // closest thing possible to the ECMAScript 5 internal IsCallable function - throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); - } - - var aArgs = Array.prototype.slice.call(arguments, 1), - fToBind = this, - fNOP = function () {}, - fBound = function () { - return fToBind.apply(this instanceof fNOP && oThis - ? this - : oThis, - aArgs.concat(Array.prototype.slice.call(arguments))); - }; + Function.prototype.bind = function (oThis) { + if (typeof this !== "function") { + // closest thing possible to the ECMAScript 5 internal IsCallable function + throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); + } + + var aArgs = Array.prototype.slice.call(arguments, 1), + fToBind = this, + fNOP = function () {}, + fBound = function () { + return fToBind.apply(this instanceof fNOP && oThis + ? this + : oThis, + aArgs.concat(Array.prototype.slice.call(arguments))); + }; - fNOP.prototype = this.prototype; - fBound.prototype = new fNOP(); + fNOP.prototype = this.prototype; + fBound.prototype = new fNOP(); - return fBound; - }; + return fBound; + }; } /** @@ -4045,7 +3506,7 @@ var util = {}; * @return {Boolean} isNumber */ util.isNumber = function isNumber(object) { - return (object instanceof Number || typeof object == 'number'); + return (object instanceof Number || typeof object == 'number'); }; /** @@ -4054,7 +3515,7 @@ util.isNumber = function isNumber(object) { * @return {Boolean} isString */ util.isString = function isString(object) { - return (object instanceof String || typeof object == 'string'); + return (object instanceof String || typeof object == 'string'); }; /** @@ -4063,21 +3524,21 @@ util.isString = function isString(object) { * @return {Boolean} isDate */ util.isDate = function isDate(object) { - if (object instanceof Date) { - return true; + if (object instanceof Date) { + return true; + } + else if (util.isString(object)) { + // test whether this string contains a date + var match = ASPDateRegex.exec(object); + if (match) { + return true; } - else if (util.isString(object)) { - // test whether this string contains a date - var match = ASPDateRegex.exec(object); - if (match) { - return true; - } - else if (!isNaN(Date.parse(object))) { - return true; - } + else if (!isNaN(Date.parse(object))) { + return true; } + } - return false; + return false; }; /** @@ -4086,10 +3547,10 @@ util.isDate = function isDate(object) { * @return {Boolean} isDataTable */ util.isDataTable = function isDataTable(object) { - return (typeof (google) !== 'undefined') && - (google.visualization) && - (google.visualization.DataTable) && - (object instanceof google.visualization.DataTable); + return (typeof (google) !== 'undefined') && + (google.visualization) && + (google.visualization.DataTable) && + (object instanceof google.visualization.DataTable); }; /** @@ -4098,19 +3559,19 @@ util.isDataTable = function isDataTable(object) { * @return {String} uuid */ util.randomUUID = function randomUUID () { - var S4 = function () { - return Math.floor( - Math.random() * 0x10000 /* 65536 */ - ).toString(16); - }; + var S4 = function () { + return Math.floor( + Math.random() * 0x10000 /* 65536 */ + ).toString(16); + }; - return ( - S4() + S4() + '-' + - S4() + '-' + - S4() + '-' + - S4() + '-' + - S4() + S4() + S4() - ); + return ( + S4() + S4() + '-' + + S4() + '-' + + S4() + '-' + + S4() + '-' + + S4() + S4() + S4() + ); }; /** @@ -4121,16 +3582,16 @@ util.randomUUID = function randomUUID () { * @return {Object} a */ util.extend = function (a, b) { - for (var i = 1, len = arguments.length; i < len; i++) { - var other = arguments[i]; - for (var prop in other) { - if (other.hasOwnProperty(prop) && other[prop] !== undefined) { - a[prop] = other[prop]; - } - } + for (var i = 1, len = arguments.length; i < len; i++) { + var other = arguments[i]; + for (var prop in other) { + if (other.hasOwnProperty(prop) && other[prop] !== undefined) { + a[prop] = other[prop]; + } } + } - return a; + return a; }; /** @@ -4143,143 +3604,143 @@ util.extend = function (a, b) { * @throws Error */ util.convert = function convert(object, type) { - var match; + var match; - if (object === undefined) { - return undefined; - } - if (object === null) { - return null; - } + if (object === undefined) { + return undefined; + } + if (object === null) { + return null; + } - if (!type) { - return object; - } - if (!(typeof type === 'string') && !(type instanceof String)) { - throw new Error('Type must be a string'); - } + if (!type) { + return object; + } + if (!(typeof type === 'string') && !(type instanceof String)) { + throw new Error('Type must be a string'); + } - //noinspection FallthroughInSwitchStatementJS - switch (type) { - case 'boolean': - case 'Boolean': - return Boolean(object); - - case 'number': - case 'Number': - return Number(object.valueOf()); - - case 'string': - case 'String': - return String(object); - - case 'Date': - if (util.isNumber(object)) { - return new Date(object); - } - if (object instanceof Date) { - return new Date(object.valueOf()); - } - else if (moment.isMoment(object)) { - return new Date(object.valueOf()); - } - if (util.isString(object)) { - match = ASPDateRegex.exec(object); - if (match) { - // object is an ASP date - return new Date(Number(match[1])); // parse number - } - else { - return moment(object).toDate(); // parse string - } - } - else { - throw new Error( - 'Cannot convert object of type ' + util.getType(object) + - ' to type Date'); - } + //noinspection FallthroughInSwitchStatementJS + switch (type) { + case 'boolean': + case 'Boolean': + return Boolean(object); - case 'Moment': - if (util.isNumber(object)) { - return moment(object); - } - if (object instanceof Date) { - return moment(object.valueOf()); - } - else if (moment.isMoment(object)) { - return moment(object); - } - if (util.isString(object)) { - match = ASPDateRegex.exec(object); - if (match) { - // object is an ASP date - return moment(Number(match[1])); // parse number - } - else { - return moment(object); // parse string - } - } - else { - throw new Error( - 'Cannot convert object of type ' + util.getType(object) + - ' to type Date'); - } + case 'number': + case 'Number': + return Number(object.valueOf()); - case 'ISODate': - if (util.isNumber(object)) { - return new Date(object); - } - else if (object instanceof Date) { - return object.toISOString(); - } - else if (moment.isMoment(object)) { - return object.toDate().toISOString(); - } - else if (util.isString(object)) { - match = ASPDateRegex.exec(object); - if (match) { - // object is an ASP date - return new Date(Number(match[1])).toISOString(); // parse number - } - else { - return new Date(object).toISOString(); // parse string - } - } - else { - throw new Error( - 'Cannot convert object of type ' + util.getType(object) + - ' to type ISODate'); - } + case 'string': + case 'String': + return String(object); - case 'ASPDate': - if (util.isNumber(object)) { - return '/Date(' + object + ')/'; - } - else if (object instanceof Date) { - return '/Date(' + object.valueOf() + ')/'; - } - else if (util.isString(object)) { - match = ASPDateRegex.exec(object); - var value; - if (match) { - // object is an ASP date - value = new Date(Number(match[1])).valueOf(); // parse number - } - else { - value = new Date(object).valueOf(); // parse string - } - return '/Date(' + value + ')/'; - } - else { - throw new Error( - 'Cannot convert object of type ' + util.getType(object) + - ' to type ASPDate'); - } + case 'Date': + if (util.isNumber(object)) { + return new Date(object); + } + if (object instanceof Date) { + return new Date(object.valueOf()); + } + else if (moment.isMoment(object)) { + return new Date(object.valueOf()); + } + if (util.isString(object)) { + match = ASPDateRegex.exec(object); + if (match) { + // object is an ASP date + return new Date(Number(match[1])); // parse number + } + else { + return moment(object).toDate(); // parse string + } + } + else { + throw new Error( + 'Cannot convert object of type ' + util.getType(object) + + ' to type Date'); + } - default: - throw new Error('Cannot convert object of type ' + util.getType(object) + - ' to type "' + type + '"'); - } + case 'Moment': + if (util.isNumber(object)) { + return moment(object); + } + if (object instanceof Date) { + return moment(object.valueOf()); + } + else if (moment.isMoment(object)) { + return moment(object); + } + if (util.isString(object)) { + match = ASPDateRegex.exec(object); + if (match) { + // object is an ASP date + return moment(Number(match[1])); // parse number + } + else { + return moment(object); // parse string + } + } + else { + throw new Error( + 'Cannot convert object of type ' + util.getType(object) + + ' to type Date'); + } + + case 'ISODate': + if (util.isNumber(object)) { + return new Date(object); + } + else if (object instanceof Date) { + return object.toISOString(); + } + else if (moment.isMoment(object)) { + return object.toDate().toISOString(); + } + else if (util.isString(object)) { + match = ASPDateRegex.exec(object); + if (match) { + // object is an ASP date + return new Date(Number(match[1])).toISOString(); // parse number + } + else { + return new Date(object).toISOString(); // parse string + } + } + else { + throw new Error( + 'Cannot convert object of type ' + util.getType(object) + + ' to type ISODate'); + } + + case 'ASPDate': + if (util.isNumber(object)) { + return '/Date(' + object + ')/'; + } + else if (object instanceof Date) { + return '/Date(' + object.valueOf() + ')/'; + } + else if (util.isString(object)) { + match = ASPDateRegex.exec(object); + var value; + if (match) { + // object is an ASP date + value = new Date(Number(match[1])).valueOf(); // parse number + } + else { + value = new Date(object).valueOf(); // parse string + } + return '/Date(' + value + ')/'; + } + else { + throw new Error( + 'Cannot convert object of type ' + util.getType(object) + + ' to type ASPDate'); + } + + default: + throw new Error('Cannot convert object of type ' + util.getType(object) + + ' to type "' + type + '"'); + } }; // parse ASP.Net Date pattern, @@ -4293,40 +3754,40 @@ var ASPDateRegex = /^\/?Date\((\-?\d+)/i; * @return {String} type */ util.getType = function getType(object) { - var type = typeof object; + var type = typeof object; - if (type == 'object') { - if (object == null) { - return 'null'; - } - if (object instanceof Boolean) { - return 'Boolean'; - } - if (object instanceof Number) { - return 'Number'; - } - if (object instanceof String) { - return 'String'; - } - if (object instanceof Array) { - return 'Array'; - } - if (object instanceof Date) { - return 'Date'; - } - return 'Object'; + if (type == 'object') { + if (object == null) { + return 'null'; + } + if (object instanceof Boolean) { + return 'Boolean'; } - else if (type == 'number') { - return 'Number'; + if (object instanceof Number) { + return 'Number'; } - else if (type == 'boolean') { - return 'Boolean'; + if (object instanceof String) { + return 'String'; + } + if (object instanceof Array) { + return 'Array'; } - else if (type == 'string') { - return 'String'; + if (object instanceof Date) { + return 'Date'; } + return 'Object'; + } + else if (type == 'number') { + return 'Number'; + } + else if (type == 'boolean') { + return 'Boolean'; + } + else if (type == 'string') { + return 'String'; + } - return type; + return type; }; /** @@ -4336,17 +3797,17 @@ util.getType = function getType(object) { * in the browser page. */ util.getAbsoluteLeft = function getAbsoluteLeft (elem) { - var doc = document.documentElement; - var body = document.body; - - var left = elem.offsetLeft; - var e = elem.offsetParent; - while (e != null && e != body && e != doc) { - left += e.offsetLeft; - left -= e.scrollLeft; - e = e.offsetParent; - } - return left; + var doc = document.documentElement; + var body = document.body; + + var left = elem.offsetLeft; + var e = elem.offsetParent; + while (e != null && e != body && e != doc) { + left += e.offsetLeft; + left -= e.scrollLeft; + e = e.offsetParent; + } + return left; }; /** @@ -4356,17 +3817,17 @@ util.getAbsoluteLeft = function getAbsoluteLeft (elem) { * in the browser page. */ util.getAbsoluteTop = function getAbsoluteTop (elem) { - var doc = document.documentElement; - var body = document.body; - - var top = elem.offsetTop; - var e = elem.offsetParent; - while (e != null && e != body && e != doc) { - top += e.offsetTop; - top -= e.scrollTop; - e = e.offsetParent; - } - return top; + var doc = document.documentElement; + var body = document.body; + + var top = elem.offsetTop; + var e = elem.offsetParent; + while (e != null && e != body && e != doc) { + top += e.offsetTop; + top -= e.scrollTop; + e = e.offsetParent; + } + return top; }; /** @@ -4375,24 +3836,24 @@ util.getAbsoluteTop = function getAbsoluteTop (elem) { * @return {Number} pageY */ util.getPageY = function getPageY (event) { - if ('pageY' in event) { - return event.pageY; + if ('pageY' in event) { + return event.pageY; + } + else { + var clientY; + if (('targetTouches' in event) && event.targetTouches.length) { + clientY = event.targetTouches[0].clientY; } else { - var clientY; - if (('targetTouches' in event) && event.targetTouches.length) { - clientY = event.targetTouches[0].clientY; - } - else { - clientY = event.clientY; - } - - var doc = document.documentElement; - var body = document.body; - return clientY + - ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - - ( doc && doc.clientTop || body && body.clientTop || 0 ); + clientY = event.clientY; } + + var doc = document.documentElement; + var body = document.body; + return clientY + + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - + ( doc && doc.clientTop || body && body.clientTop || 0 ); + } }; /** @@ -4401,24 +3862,24 @@ util.getPageY = function getPageY (event) { * @return {Number} pageX */ util.getPageX = function getPageX (event) { - if ('pageY' in event) { - return event.pageX; + if ('pageY' in event) { + return event.pageX; + } + else { + var clientX; + if (('targetTouches' in event) && event.targetTouches.length) { + clientX = event.targetTouches[0].clientX; } else { - var clientX; - if (('targetTouches' in event) && event.targetTouches.length) { - clientX = event.targetTouches[0].clientX; - } - else { - clientX = event.clientX; - } - - var doc = document.documentElement; - var body = document.body; - return clientX + - ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + clientX = event.clientX; } + + var doc = document.documentElement; + var body = document.body; + return clientX + + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - + ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + } }; /** @@ -4427,11 +3888,11 @@ util.getPageX = function getPageX (event) { * @param {String} className */ util.addClassName = function addClassName(elem, className) { - var classes = elem.className.split(' '); - if (classes.indexOf(className) == -1) { - classes.push(className); // add the class to the array - elem.className = classes.join(' '); - } + var classes = elem.className.split(' '); + if (classes.indexOf(className) == -1) { + classes.push(className); // add the class to the array + elem.className = classes.join(' '); + } }; /** @@ -4440,12 +3901,12 @@ util.addClassName = function addClassName(elem, className) { * @param {String} className */ util.removeClassName = function removeClassname(elem, className) { - var classes = elem.className.split(' '); - var index = classes.indexOf(className); - if (index != -1) { - classes.splice(index, 1); // remove the class from the array - elem.className = classes.join(' '); - } + var classes = elem.className.split(' '); + var index = classes.indexOf(className); + if (index != -1) { + classes.splice(index, 1); // remove the class from the array + elem.className = classes.join(' '); + } }; /** @@ -4458,22 +3919,22 @@ util.removeClassName = function removeClassname(elem, className) { * callback(value, index, object) */ util.forEach = function forEach (object, callback) { - var i, - len; - if (object instanceof Array) { - // array - for (i = 0, len = object.length; i < len; i++) { - callback(object[i], i, object); - } + var i, + len; + if (object instanceof Array) { + // array + for (i = 0, len = object.length; i < len; i++) { + callback(object[i], i, object); } - else { - // object - for (i in object) { - if (object.hasOwnProperty(i)) { - callback(object[i], i, object); - } - } + } + else { + // object + for (i in object) { + if (object.hasOwnProperty(i)) { + callback(object[i], i, object); + } } + } }; /** @@ -4484,13 +3945,13 @@ util.forEach = function forEach (object, callback) { * @return {Boolean} changed */ util.updateProperty = function updateProp (object, key, value) { - if (object[key] !== value) { - object[key] = value; - return true; - } - else { - return false; - } + if (object[key] !== value) { + object[key] = value; + return true; + } + else { + return false; + } }; /** @@ -4502,18 +3963,18 @@ util.updateProperty = function updateProp (object, key, value) { * @param {boolean} [useCapture] */ util.addEventListener = function addEventListener(element, action, listener, useCapture) { - if (element.addEventListener) { - if (useCapture === undefined) - useCapture = false; + if (element.addEventListener) { + if (useCapture === undefined) + useCapture = false; - if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { - action = "DOMMouseScroll"; // For Firefox - } - - element.addEventListener(action, listener, useCapture); - } else { - element.attachEvent("on" + action, listener); // IE browsers + if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { + action = "DOMMouseScroll"; // For Firefox } + + element.addEventListener(action, listener, useCapture); + } else { + element.attachEvent("on" + action, listener); // IE browsers + } }; /** @@ -4524,20 +3985,20 @@ util.addEventListener = function addEventListener(element, action, listener, use * @param {boolean} [useCapture] */ util.removeEventListener = function removeEventListener(element, action, listener, useCapture) { - if (element.removeEventListener) { - // non-IE browsers - if (useCapture === undefined) - useCapture = false; + if (element.removeEventListener) { + // non-IE browsers + if (useCapture === undefined) + useCapture = false; - if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { - action = "DOMMouseScroll"; // For Firefox - } - - element.removeEventListener(action, listener, useCapture); - } else { - // IE browsers - element.detachEvent("on" + action, listener); + if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { + action = "DOMMouseScroll"; // For Firefox } + + element.removeEventListener(action, listener, useCapture); + } else { + // IE browsers + element.detachEvent("on" + action, listener); + } }; @@ -4547,41 +4008,41 @@ util.removeEventListener = function removeEventListener(element, action, listene * @return {Element} target element */ util.getTarget = function getTarget(event) { - // code from http://www.quirksmode.org/js/events_properties.html - if (!event) { - event = window.event; - } + // code from http://www.quirksmode.org/js/events_properties.html + if (!event) { + event = window.event; + } - var target; + var target; - if (event.target) { - target = event.target; - } - else if (event.srcElement) { - target = event.srcElement; - } + if (event.target) { + target = event.target; + } + else if (event.srcElement) { + target = event.srcElement; + } - if (target.nodeType != undefined && target.nodeType == 3) { - // defeat Safari bug - target = target.parentNode; - } + if (target.nodeType != undefined && target.nodeType == 3) { + // defeat Safari bug + target = target.parentNode; + } - return target; + return target; }; /** * Stop event propagation */ util.stopPropagation = function stopPropagation(event) { - if (!event) - event = window.event; + if (!event) + event = window.event; - if (event.stopPropagation) { - event.stopPropagation(); // non-IE browsers - } - else { - event.cancelBubble = true; // IE browsers - } + if (event.stopPropagation) { + event.stopPropagation(); // non-IE browsers + } + else { + event.cancelBubble = true; // IE browsers + } }; @@ -4589,15 +4050,15 @@ util.stopPropagation = function stopPropagation(event) { * Cancels the event if it is cancelable, without stopping further propagation of the event. */ util.preventDefault = function preventDefault (event) { - if (!event) - event = window.event; + if (!event) + event = window.event; - if (event.preventDefault) { - event.preventDefault(); // non-IE browsers - } - else { - event.returnValue = false; // IE browsers - } + if (event.preventDefault) { + event.preventDefault(); // non-IE browsers + } + else { + event.returnValue = false; // IE browsers + } }; @@ -4610,15 +4071,15 @@ util.option = {}; * @returns {Boolean} bool */ util.option.asBoolean = function (value, defaultValue) { - if (typeof value == 'function') { - value = value(); - } + if (typeof value == 'function') { + value = value(); + } - if (value != null) { - return (value != false); - } + if (value != null) { + return (value != false); + } - return defaultValue || null; + return defaultValue || null; }; /** @@ -4628,15 +4089,15 @@ util.option.asBoolean = function (value, defaultValue) { * @returns {Number} number */ util.option.asNumber = function (value, defaultValue) { - if (typeof value == 'function') { - value = value(); - } + if (typeof value == 'function') { + value = value(); + } - if (value != null) { - return Number(value) || defaultValue || null; - } + if (value != null) { + return Number(value) || defaultValue || null; + } - return defaultValue || null; + return defaultValue || null; }; /** @@ -4646,15 +4107,15 @@ util.option.asNumber = function (value, defaultValue) { * @returns {String} str */ util.option.asString = function (value, defaultValue) { - if (typeof value == 'function') { - value = value(); - } + if (typeof value == 'function') { + value = value(); + } - if (value != null) { - return String(value); - } + if (value != null) { + return String(value); + } - return defaultValue || null; + return defaultValue || null; }; /** @@ -4664,19 +4125,19 @@ util.option.asString = function (value, defaultValue) { * @returns {String} size */ util.option.asSize = function (value, defaultValue) { - if (typeof value == 'function') { - value = value(); - } + if (typeof value == 'function') { + value = value(); + } - if (util.isString(value)) { - return value; - } - else if (util.isNumber(value)) { - return value + 'px'; - } - else { - return defaultValue || null; - } + if (util.isString(value)) { + return value; + } + else if (util.isNumber(value)) { + return value + 'px'; + } + else { + return defaultValue || null; + } }; /** @@ -4686,11 +4147,11 @@ util.option.asSize = function (value, defaultValue) { * @returns {HTMLElement | null} dom */ util.option.asElement = function (value, defaultValue) { - if (typeof value == 'function') { - value = value(); - } + if (typeof value == 'function') { + value = value(); + } - return value || defaultValue || null; + return value || defaultValue || null; }; /** @@ -4698,27 +4159,27 @@ util.option.asElement = function (value, defaultValue) { * @param {String} css Text containing css */ util.loadCss = function (css) { - if (typeof document === 'undefined') { - return; - } - - // get the script location, and built the css file name from the js file name - // http://stackoverflow.com/a/2161748/1262753 - // var scripts = document.getElementsByTagName('script'); - // var jsFile = scripts[scripts.length-1].src.split('?')[0]; - // var cssFile = jsFile.substring(0, jsFile.length - 2) + 'css'; + if (typeof document === 'undefined') { + return; + } - // inject css - // http://stackoverflow.com/questions/524696/how-to-create-a-style-tag-with-javascript - var style = document.createElement('style'); - style.type = 'text/css'; - if (style.styleSheet){ - style.styleSheet.cssText = css; - } else { - style.appendChild(document.createTextNode(css)); - } + // get the script location, and built the css file name from the js file name + // http://stackoverflow.com/a/2161748/1262753 + // var scripts = document.getElementsByTagName('script'); + // var jsFile = scripts[scripts.length-1].src.split('?')[0]; + // var cssFile = jsFile.substring(0, jsFile.length - 2) + 'css'; + + // inject css + // http://stackoverflow.com/questions/524696/how-to-create-a-style-tag-with-javascript + var style = document.createElement('style'); + style.type = 'text/css'; + if (style.styleSheet){ + style.styleSheet.cssText = css; + } else { + style.appendChild(document.createTextNode(css)); + } - document.getElementsByTagName('head')[0].appendChild(style); + document.getElementsByTagName('head')[0].appendChild(style); }; /** @@ -4726,116 +4187,116 @@ util.loadCss = function (css) { */ // TODO: replace usage of the event listener for the EventBus var events = { - 'listeners': [], - - /** - * Find a single listener by its object - * @param {Object} object - * @return {Number} index -1 when not found - */ - 'indexOf': function (object) { - var listeners = this.listeners; - for (var i = 0, iMax = this.listeners.length; i < iMax; i++) { - var listener = listeners[i]; - if (listener && listener.object == object) { - return i; - } - } - return -1; - }, - - /** - * Add an event listener - * @param {Object} object - * @param {String} event The name of an event, for example 'select' - * @param {function} callback The callback method, called when the - * event takes place - */ - 'addListener': function (object, event, callback) { - var index = this.indexOf(object); - var listener = this.listeners[index]; - if (!listener) { - listener = { - 'object': object, - 'events': {} - }; - this.listeners.push(listener); - } - - var callbacks = listener.events[event]; - if (!callbacks) { - callbacks = []; - listener.events[event] = callbacks; - } - - // add the callback if it does not yet exist - if (callbacks.indexOf(callback) == -1) { - callbacks.push(callback); + 'listeners': [], + + /** + * Find a single listener by its object + * @param {Object} object + * @return {Number} index -1 when not found + */ + 'indexOf': function (object) { + var listeners = this.listeners; + for (var i = 0, iMax = this.listeners.length; i < iMax; i++) { + var listener = listeners[i]; + if (listener && listener.object == object) { + return i; + } + } + return -1; + }, + + /** + * Add an event listener + * @param {Object} object + * @param {String} event The name of an event, for example 'select' + * @param {function} callback The callback method, called when the + * event takes place + */ + 'addListener': function (object, event, callback) { + var index = this.indexOf(object); + var listener = this.listeners[index]; + if (!listener) { + listener = { + 'object': object, + 'events': {} + }; + this.listeners.push(listener); + } + + var callbacks = listener.events[event]; + if (!callbacks) { + callbacks = []; + listener.events[event] = callbacks; + } + + // add the callback if it does not yet exist + if (callbacks.indexOf(callback) == -1) { + callbacks.push(callback); + } + }, + + /** + * Remove an event listener + * @param {Object} object + * @param {String} event The name of an event, for example 'select' + * @param {function} callback The registered callback method + */ + 'removeListener': function (object, event, callback) { + var index = this.indexOf(object); + var listener = this.listeners[index]; + if (listener) { + var callbacks = listener.events[event]; + if (callbacks) { + index = callbacks.indexOf(callback); + if (index != -1) { + callbacks.splice(index, 1); + } + + // remove the array when empty + if (callbacks.length == 0) { + delete listener.events[event]; } - }, - - /** - * Remove an event listener - * @param {Object} object - * @param {String} event The name of an event, for example 'select' - * @param {function} callback The registered callback method - */ - 'removeListener': function (object, event, callback) { - var index = this.indexOf(object); - var listener = this.listeners[index]; - if (listener) { - var callbacks = listener.events[event]; - if (callbacks) { - index = callbacks.indexOf(callback); - if (index != -1) { - callbacks.splice(index, 1); - } - - // remove the array when empty - if (callbacks.length == 0) { - delete listener.events[event]; - } - } + } - // count the number of registered events. remove listener when empty - var count = 0; - var events = listener.events; - for (var e in events) { - if (events.hasOwnProperty(e)) { - count++; - } - } - if (count == 0) { - delete this.listeners[index]; - } + // count the number of registered events. remove listener when empty + var count = 0; + var events = listener.events; + for (var e in events) { + if (events.hasOwnProperty(e)) { + count++; } - }, - - /** - * Remove all registered event listeners - */ - 'removeAllListeners': function () { - this.listeners = []; - }, + } + if (count == 0) { + delete this.listeners[index]; + } + } + }, - /** - * Trigger an event. All registered event handlers will be called - * @param {Object} object - * @param {String} event - * @param {Object} properties (optional) - */ - 'trigger': function (object, event, properties) { - var index = this.indexOf(object); - var listener = this.listeners[index]; - if (listener) { - var callbacks = listener.events[event]; - if (callbacks) { - for (var i = 0, iMax = callbacks.length; i < iMax; i++) { - callbacks[i](properties); - } - } + /** + * Remove all registered event listeners + */ + 'removeAllListeners': function () { + this.listeners = []; + }, + + /** + * Trigger an event. All registered event handlers will be called + * @param {Object} object + * @param {String} event + * @param {Object} properties (optional) + */ + 'trigger': function (object, event, properties) { + var index = this.indexOf(object); + var listener = this.listeners[index]; + if (listener) { + var callbacks = listener.events[event]; + if (callbacks) { + for (var i = 0, iMax = callbacks.length; i < iMax; i++) { + callbacks[i](properties); } + } } + } }; /** @@ -4843,7 +4304,7 @@ var events = { * @constructor EventBus */ function EventBus() { - this.subscriptions = []; + this.subscriptions = []; } /** @@ -4856,21 +4317,21 @@ function EventBus() { * @returns {String} id A subscription id */ EventBus.prototype.on = function (event, callback, target) { - var regexp = (event instanceof RegExp) ? - event : - new RegExp(event.replace('*', '\\w+')); - - var subscription = { - id: util.randomUUID(), - event: event, - regexp: regexp, - callback: (typeof callback === 'function') ? callback : null, - target: target - }; + var regexp = (event instanceof RegExp) ? + event : + new RegExp(event.replace('*', '\\w+')); + + var subscription = { + id: util.randomUUID(), + event: event, + regexp: regexp, + callback: (typeof callback === 'function') ? callback : null, + target: target + }; - this.subscriptions.push(subscription); + this.subscriptions.push(subscription); - return subscription.id; + return subscription.id; }; /** @@ -4882,33 +4343,33 @@ EventBus.prototype.on = function (event, callback, target) { * callback, and target. */ EventBus.prototype.off = function (filter) { - var i = 0; - while (i < this.subscriptions.length) { - var subscription = this.subscriptions[i]; - - var match = true; - if (filter instanceof Object) { - // filter is an object. All fields must match - for (var prop in filter) { - if (filter.hasOwnProperty(prop)) { - if (filter[prop] !== subscription[prop]) { - match = false; - } - } - } - } - else { - // filter is a string, filter on id - match = (subscription.id == filter); + var i = 0; + while (i < this.subscriptions.length) { + var subscription = this.subscriptions[i]; + + var match = true; + if (filter instanceof Object) { + // filter is an object. All fields must match + for (var prop in filter) { + if (filter.hasOwnProperty(prop)) { + if (filter[prop] !== subscription[prop]) { + match = false; + } } + } + } + else { + // filter is a string, filter on id + match = (subscription.id == filter); + } - if (match) { - this.subscriptions.splice(i, 1); - } - else { - i++; - } + if (match) { + this.subscriptions.splice(i, 1); } + else { + i++; + } + } }; /** @@ -4918,14 +4379,14 @@ EventBus.prototype.off = function (filter) { * @param {*} [source] */ EventBus.prototype.emit = function (event, data, source) { - for (var i =0; i < this.subscriptions.length; i++) { - var subscription = this.subscriptions[i]; - if (subscription.regexp.test(event)) { - if (subscription.callback) { - subscription.callback(event, data, source); - } - } + for (var i =0; i < this.subscriptions.length; i++) { + var subscription = this.subscriptions[i]; + if (subscription.regexp.test(event)) { + if (subscription.callback) { + subscription.callback(event, data, source); + } } + } }; /** @@ -4966,31 +4427,31 @@ EventBus.prototype.emit = function (event, data, source) { */ // TODO: add a DataSet constructor DataSet(data, options) function DataSet (options) { - this.id = util.randomUUID(); - - this.options = options || {}; - this.data = {}; // map with data indexed by id - this.fieldId = this.options.fieldId || 'id'; // name of the field containing id - this.convert = {}; // field types by field name - - if (this.options.convert) { - for (var field in this.options.convert) { - if (this.options.convert.hasOwnProperty(field)) { - var value = this.options.convert[field]; - if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') { - this.convert[field] = 'Date'; - } - else { - this.convert[field] = value; - } - } + this.id = util.randomUUID(); + + this.options = options || {}; + this.data = {}; // map with data indexed by id + this.fieldId = this.options.fieldId || 'id'; // name of the field containing id + this.convert = {}; // field types by field name + + if (this.options.convert) { + for (var field in this.options.convert) { + if (this.options.convert.hasOwnProperty(field)) { + var value = this.options.convert[field]; + if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') { + this.convert[field] = 'Date'; + } + else { + this.convert[field] = value; } + } } + } - // event subscribers - this.subscribers = {}; + // event subscribers + this.subscribers = {}; - this.internalIds = {}; // internally generated id's + this.internalIds = {}; // internally generated id's } /** @@ -5003,15 +4464,15 @@ function DataSet (options) { * {String | Number} senderId */ DataSet.prototype.subscribe = function (event, callback) { - var subscribers = this.subscribers[event]; - if (!subscribers) { - subscribers = []; - this.subscribers[event] = subscribers; - } + var subscribers = this.subscribers[event]; + if (!subscribers) { + subscribers = []; + this.subscribers[event] = subscribers; + } - subscribers.push({ - callback: callback - }); + subscribers.push({ + callback: callback + }); }; /** @@ -5020,12 +4481,12 @@ DataSet.prototype.subscribe = function (event, callback) { * @param {function} callback */ DataSet.prototype.unsubscribe = function (event, callback) { - var subscribers = this.subscribers[event]; - if (subscribers) { - this.subscribers[event] = subscribers.filter(function (listener) { - return (listener.callback != callback); - }); - } + var subscribers = this.subscribers[event]; + if (subscribers) { + this.subscribers[event] = subscribers.filter(function (listener) { + return (listener.callback != callback); + }); + } }; /** @@ -5036,24 +4497,24 @@ DataSet.prototype.unsubscribe = function (event, callback) { * @private */ DataSet.prototype._trigger = function (event, params, senderId) { - if (event == '*') { - throw new Error('Cannot trigger event *'); - } + if (event == '*') { + throw new Error('Cannot trigger event *'); + } - var subscribers = []; - if (event in this.subscribers) { - subscribers = subscribers.concat(this.subscribers[event]); - } - if ('*' in this.subscribers) { - subscribers = subscribers.concat(this.subscribers['*']); - } + var subscribers = []; + if (event in this.subscribers) { + subscribers = subscribers.concat(this.subscribers[event]); + } + if ('*' in this.subscribers) { + subscribers = subscribers.concat(this.subscribers['*']); + } - for (var i = 0; i < subscribers.length; i++) { - var subscriber = subscribers[i]; - if (subscriber.callback) { - subscriber.callback(event, params, senderId || null); - } + for (var i = 0; i < subscribers.length; i++) { + var subscriber = subscribers[i]; + if (subscriber.callback) { + subscriber.callback(event, params, senderId || null); } + } }; /** @@ -5064,45 +4525,45 @@ DataSet.prototype._trigger = function (event, params, senderId) { * @return {Array} addedIds Array with the ids of the added items */ DataSet.prototype.add = function (data, senderId) { - var addedIds = [], - id, - me = this; - - if (data instanceof Array) { - // Array - for (var i = 0, len = data.length; i < len; i++) { - id = me._addItem(data[i]); - addedIds.push(id); - } - } - else if (util.isDataTable(data)) { - // Google DataTable - var columns = this._getColumnNames(data); - for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) { - var item = {}; - for (var col = 0, cols = columns.length; col < cols; col++) { - var field = columns[col]; - item[field] = data.getValue(row, col); - } + var addedIds = [], + id, + me = this; - id = me._addItem(item); - addedIds.push(id); - } - } - else if (data instanceof Object) { - // Single item - id = me._addItem(data); - addedIds.push(id); - } - else { - throw new Error('Unknown dataType'); + if (data instanceof Array) { + // Array + for (var i = 0, len = data.length; i < len; i++) { + id = me._addItem(data[i]); + addedIds.push(id); } + } + else if (util.isDataTable(data)) { + // Google DataTable + var columns = this._getColumnNames(data); + for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) { + var item = {}; + for (var col = 0, cols = columns.length; col < cols; col++) { + var field = columns[col]; + item[field] = data.getValue(row, col); + } - if (addedIds.length) { - this._trigger('add', {items: addedIds}, senderId); + id = me._addItem(item); + addedIds.push(id); } + } + else if (data instanceof Object) { + // Single item + id = me._addItem(data); + addedIds.push(id); + } + else { + throw new Error('Unknown dataType'); + } + + if (addedIds.length) { + this._trigger('add', {items: addedIds}, senderId); + } - return addedIds; + return addedIds; }; /** @@ -5112,60 +4573,60 @@ DataSet.prototype.add = function (data, senderId) { * @return {Array} updatedIds The ids of the added or updated items */ DataSet.prototype.update = function (data, senderId) { - var addedIds = [], - updatedIds = [], - me = this, - fieldId = me.fieldId; - - var addOrUpdate = function (item) { - var id = item[fieldId]; - if (me.data[id]) { - // update item - id = me._updateItem(item); - updatedIds.push(id); - } - else { - // add new item - id = me._addItem(item); - addedIds.push(id); - } - }; - - if (data instanceof Array) { - // Array - for (var i = 0, len = data.length; i < len; i++) { - addOrUpdate(data[i]); - } - } - else if (util.isDataTable(data)) { - // Google DataTable - var columns = this._getColumnNames(data); - for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) { - var item = {}; - for (var col = 0, cols = columns.length; col < cols; col++) { - var field = columns[col]; - item[field] = data.getValue(row, col); - } - - addOrUpdate(item); - } - } - else if (data instanceof Object) { - // Single item - addOrUpdate(data); + var addedIds = [], + updatedIds = [], + me = this, + fieldId = me.fieldId; + + var addOrUpdate = function (item) { + var id = item[fieldId]; + if (me.data[id]) { + // update item + id = me._updateItem(item); + updatedIds.push(id); } else { - throw new Error('Unknown dataType'); + // add new item + id = me._addItem(item); + addedIds.push(id); } + }; - if (addedIds.length) { - this._trigger('add', {items: addedIds}, senderId); + if (data instanceof Array) { + // Array + for (var i = 0, len = data.length; i < len; i++) { + addOrUpdate(data[i]); } - if (updatedIds.length) { - this._trigger('update', {items: updatedIds}, senderId); + } + else if (util.isDataTable(data)) { + // Google DataTable + var columns = this._getColumnNames(data); + for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) { + var item = {}; + for (var col = 0, cols = columns.length; col < cols; col++) { + var field = columns[col]; + item[field] = data.getValue(row, col); + } + + addOrUpdate(item); } + } + else if (data instanceof Object) { + // Single item + addOrUpdate(data); + } + else { + throw new Error('Unknown dataType'); + } - return addedIds.concat(updatedIds); + if (addedIds.length) { + this._trigger('add', {items: addedIds}, senderId); + } + if (updatedIds.length) { + this._trigger('update', {items: updatedIds}, senderId); + } + + return addedIds.concat(updatedIds); }; /** @@ -5204,138 +4665,138 @@ DataSet.prototype.update = function (data, senderId) { * @throws Error */ DataSet.prototype.get = function (args) { - var me = this; + var me = this; + + // parse the arguments + var id, ids, options, data; + var firstType = util.getType(arguments[0]); + if (firstType == 'String' || firstType == 'Number') { + // get(id [, options] [, data]) + id = arguments[0]; + options = arguments[1]; + data = arguments[2]; + } + else if (firstType == 'Array') { + // get(ids [, options] [, data]) + ids = arguments[0]; + options = arguments[1]; + data = arguments[2]; + } + else { + // get([, options] [, data]) + options = arguments[0]; + data = arguments[1]; + } + + // determine the return type + var type; + if (options && options.type) { + type = (options.type == 'DataTable') ? 'DataTable' : 'Array'; - // parse the arguments - var id, ids, options, data; - var firstType = util.getType(arguments[0]); - if (firstType == 'String' || firstType == 'Number') { - // get(id [, options] [, data]) - id = arguments[0]; - options = arguments[1]; - data = arguments[2]; - } - else if (firstType == 'Array') { - // get(ids [, options] [, data]) - ids = arguments[0]; - options = arguments[1]; - data = arguments[2]; + if (data && (type != util.getType(data))) { + throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' + + 'does not correspond with specified options.type (' + options.type + ')'); } - else { - // get([, options] [, data]) - options = arguments[0]; - data = arguments[1]; + if (type == 'DataTable' && !util.isDataTable(data)) { + throw new Error('Parameter "data" must be a DataTable ' + + 'when options.type is "DataTable"'); } + } + else if (data) { + type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array'; + } + else { + type = 'Array'; + } - // determine the return type - var type; - if (options && options.type) { - type = (options.type == 'DataTable') ? 'DataTable' : 'Array'; + // build options + var convert = options && options.convert || this.options.convert; + var filter = options && options.filter; + var items = [], item, itemId, i, len; - if (data && (type != util.getType(data))) { - throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' + - 'does not correspond with specified options.type (' + options.type + ')'); - } - if (type == 'DataTable' && !util.isDataTable(data)) { - throw new Error('Parameter "data" must be a DataTable ' + - 'when options.type is "DataTable"'); - } + // convert items + if (id != undefined) { + // return a single item + item = me._getItem(id, convert); + if (filter && !filter(item)) { + item = null; } - else if (data) { - type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array'; + } + else if (ids != undefined) { + // return a subset of items + for (i = 0, len = ids.length; i < len; i++) { + item = me._getItem(ids[i], convert); + if (!filter || filter(item)) { + items.push(item); + } } - else { - type = 'Array'; + } + else { + // return all items + for (itemId in this.data) { + if (this.data.hasOwnProperty(itemId)) { + item = me._getItem(itemId, convert); + if (!filter || filter(item)) { + items.push(item); + } + } } + } - // build options - var convert = options && options.convert || this.options.convert; - var filter = options && options.filter; - var items = [], item, itemId, i, len; + // order the results + if (options && options.order && id == undefined) { + this._sort(items, options.order); + } - // convert items + // filter fields of the items + if (options && options.fields) { + var fields = options.fields; if (id != undefined) { - // return a single item - item = me._getItem(id, convert); - if (filter && !filter(item)) { - item = null; - } - } - else if (ids != undefined) { - // return a subset of items - for (i = 0, len = ids.length; i < len; i++) { - item = me._getItem(ids[i], convert); - if (!filter || filter(item)) { - items.push(item); - } - } + item = this._filterFields(item, fields); } else { - // return all items - for (itemId in this.data) { - if (this.data.hasOwnProperty(itemId)) { - item = me._getItem(itemId, convert); - if (!filter || filter(item)) { - items.push(item); - } - } - } + for (i = 0, len = items.length; i < len; i++) { + items[i] = this._filterFields(items[i], fields); + } } + } - // order the results - if (options && options.order && id == undefined) { - this._sort(items, options.order); + // return the results + if (type == 'DataTable') { + var columns = this._getColumnNames(data); + if (id != undefined) { + // append a single item to the data table + me._appendRow(data, columns, item); } - - // filter fields of the items - if (options && options.fields) { - var fields = options.fields; - if (id != undefined) { - item = this._filterFields(item, fields); - } - else { - for (i = 0, len = items.length; i < len; i++) { - items[i] = this._filterFields(items[i], fields); - } - } + else { + // copy the items to the provided data table + for (i = 0, len = items.length; i < len; i++) { + me._appendRow(data, columns, items[i]); + } } - - // return the results - if (type == 'DataTable') { - var columns = this._getColumnNames(data); - if (id != undefined) { - // append a single item to the data table - me._appendRow(data, columns, item); - } - else { - // copy the items to the provided data table - for (i = 0, len = items.length; i < len; i++) { - me._appendRow(data, columns, items[i]); - } - } - return data; + return data; + } + else { + // return an array + if (id != undefined) { + // a single item + return item; } else { - // return an array - if (id != undefined) { - // a single item - return item; - } - else { - // multiple items - if (data) { - // copy the items to the provided array - for (i = 0, len = items.length; i < len; i++) { - data.push(items[i]); - } - return data; - } - else { - // just return our array - return items; - } + // multiple items + if (data) { + // copy the items to the provided array + for (i = 0, len = items.length; i < len; i++) { + data.push(items[i]); } + return data; + } + else { + // just return our array + return items; + } } + } }; /** @@ -5347,78 +4808,78 @@ DataSet.prototype.get = function (args) { * @return {Array} ids */ DataSet.prototype.getIds = function (options) { - var data = this.data, - filter = options && options.filter, - order = options && options.order, - convert = options && options.convert || this.options.convert, - i, - len, - id, - item, - items, - ids = []; - - if (filter) { - // get filtered items - if (order) { - // create ordered list - items = []; - for (id in data) { - if (data.hasOwnProperty(id)) { - item = this._getItem(id, convert); - if (filter(item)) { - items.push(item); - } - } - } + var data = this.data, + filter = options && options.filter, + order = options && options.order, + convert = options && options.convert || this.options.convert, + i, + len, + id, + item, + items, + ids = []; + + if (filter) { + // get filtered items + if (order) { + // create ordered list + items = []; + for (id in data) { + if (data.hasOwnProperty(id)) { + item = this._getItem(id, convert); + if (filter(item)) { + items.push(item); + } + } + } - this._sort(items, order); + this._sort(items, order); - for (i = 0, len = items.length; i < len; i++) { - ids[i] = items[i][this.fieldId]; - } - } - else { - // create unordered list - for (id in data) { - if (data.hasOwnProperty(id)) { - item = this._getItem(id, convert); - if (filter(item)) { - ids.push(item[this.fieldId]); - } - } - } - } + for (i = 0, len = items.length; i < len; i++) { + ids[i] = items[i][this.fieldId]; + } } else { - // get all items - if (order) { - // create an ordered list - items = []; - for (id in data) { - if (data.hasOwnProperty(id)) { - items.push(data[id]); - } - } + // create unordered list + for (id in data) { + if (data.hasOwnProperty(id)) { + item = this._getItem(id, convert); + if (filter(item)) { + ids.push(item[this.fieldId]); + } + } + } + } + } + else { + // get all items + if (order) { + // create an ordered list + items = []; + for (id in data) { + if (data.hasOwnProperty(id)) { + items.push(data[id]); + } + } - this._sort(items, order); + this._sort(items, order); - for (i = 0, len = items.length; i < len; i++) { - ids[i] = 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 = items.length; i < len; i++) { + ids[i] = items[i][this.fieldId]; + } + } + else { + // create unordered list + for (id in data) { + if (data.hasOwnProperty(id)) { + item = data[id]; + ids.push(item[this.fieldId]); } + } } + } - return ids; + return ids; }; /** @@ -5433,33 +4894,33 @@ DataSet.prototype.getIds = function (options) { * a field name or custom sort function. */ DataSet.prototype.forEach = function (callback, options) { - var filter = options && options.filter, - convert = options && options.convert || this.options.convert, - data = this.data, - item, - id; + var filter = options && options.filter, + convert = options && options.convert || this.options.convert, + data = this.data, + item, + id; - if (options && options.order) { - // execute forEach on ordered list - var items = this.get(options); + if (options && options.order) { + // execute forEach on ordered list + var items = this.get(options); - for (var i = 0, len = items.length; i < len; i++) { - item = items[i]; - id = item[this.fieldId]; - callback(item, id); - } + for (var i = 0, len = items.length; i < len; i++) { + item = items[i]; + id = item[this.fieldId]; + callback(item, id); } - else { - // unordered - for (id in data) { - if (data.hasOwnProperty(id)) { - item = this._getItem(id, convert); - if (!filter || filter(item)) { - callback(item, id); - } - } + } + else { + // unordered + for (id in data) { + if (data.hasOwnProperty(id)) { + item = this._getItem(id, convert); + if (!filter || filter(item)) { + callback(item, id); } + } } + } }; /** @@ -5474,28 +4935,28 @@ DataSet.prototype.forEach = function (callback, options) { * @return {Object[]} mappedItems */ DataSet.prototype.map = function (callback, options) { - var filter = options && options.filter, - convert = options && options.convert || this.options.convert, - mappedItems = [], - data = this.data, - item; - - // convert and filter items - for (var id in data) { - if (data.hasOwnProperty(id)) { - item = this._getItem(id, convert); - if (!filter || filter(item)) { - mappedItems.push(callback(item, id)); - } - } + var filter = options && options.filter, + convert = options && options.convert || this.options.convert, + mappedItems = [], + data = this.data, + item; + + // convert and filter items + for (var id in data) { + if (data.hasOwnProperty(id)) { + item = this._getItem(id, convert); + if (!filter || filter(item)) { + mappedItems.push(callback(item, id)); + } } + } - // order items - if (options && options.order) { - this._sort(mappedItems, options.order); - } + // order items + if (options && options.order) { + this._sort(mappedItems, options.order); + } - return mappedItems; + return mappedItems; }; /** @@ -5506,15 +4967,15 @@ DataSet.prototype.map = function (callback, options) { * @private */ DataSet.prototype._filterFields = function (item, fields) { - var filteredItem = {}; + var filteredItem = {}; - for (var field in item) { - if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) { - filteredItem[field] = item[field]; - } + for (var field in item) { + if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) { + filteredItem[field] = item[field]; } + } - return filteredItem; + return filteredItem; }; /** @@ -5524,24 +4985,24 @@ DataSet.prototype._filterFields = function (item, fields) { * @private */ DataSet.prototype._sort = function (items, order) { - if (util.isString(order)) { - // order by provided field name - var name = order; // field name - items.sort(function (a, b) { - var av = a[name]; - var bv = b[name]; - return (av > bv) ? 1 : ((av < bv) ? -1 : 0); - }); - } - else if (typeof order === 'function') { - // order by sort function - items.sort(order); - } - // TODO: extend order by an Object {field:String, direction:String} - // where direction can be 'asc' or 'desc' - else { - throw new TypeError('Order must be a function or a string'); - } + if (util.isString(order)) { + // order by provided field name + var name = order; // field name + items.sort(function (a, b) { + var av = a[name]; + var bv = b[name]; + return (av > bv) ? 1 : ((av < bv) ? -1 : 0); + }); + } + else if (typeof order === 'function') { + // order by sort function + items.sort(order); + } + // TODO: extend order by an Object {field:String, direction:String} + // where direction can be 'asc' or 'desc' + else { + throw new TypeError('Order must be a function or a string'); + } }; /** @@ -5552,29 +5013,29 @@ DataSet.prototype._sort = function (items, order) { * @return {Array} removedIds */ DataSet.prototype.remove = function (id, senderId) { - var removedIds = [], - i, len, removedId; - - if (id instanceof Array) { - for (i = 0, len = id.length; i < len; i++) { - removedId = this._remove(id[i]); - if (removedId != null) { - removedIds.push(removedId); - } - } + var removedIds = [], + i, len, removedId; + + if (id instanceof Array) { + for (i = 0, len = id.length; i < len; i++) { + removedId = this._remove(id[i]); + if (removedId != null) { + removedIds.push(removedId); + } } - else { - removedId = this._remove(id); - if (removedId != null) { - removedIds.push(removedId); - } + } + else { + removedId = this._remove(id); + if (removedId != null) { + removedIds.push(removedId); } + } - if (removedIds.length) { - this._trigger('remove', {items: removedIds}, senderId); - } + if (removedIds.length) { + this._trigger('remove', {items: removedIds}, senderId); + } - return removedIds; + return removedIds; }; /** @@ -5584,22 +5045,22 @@ DataSet.prototype.remove = function (id, senderId) { * @private */ DataSet.prototype._remove = function (id) { - if (util.isNumber(id) || util.isString(id)) { - if (this.data[id]) { - delete this.data[id]; - delete this.internalIds[id]; - return id; - } + if (util.isNumber(id) || util.isString(id)) { + if (this.data[id]) { + delete this.data[id]; + delete this.internalIds[id]; + return id; } - else if (id instanceof Object) { - var itemId = id[this.fieldId]; - if (itemId && this.data[itemId]) { - delete this.data[itemId]; - delete this.internalIds[itemId]; - return itemId; - } + } + else if (id instanceof Object) { + var itemId = id[this.fieldId]; + if (itemId && this.data[itemId]) { + delete this.data[itemId]; + delete this.internalIds[itemId]; + return itemId; } - return null; + } + return null; }; /** @@ -5608,14 +5069,14 @@ DataSet.prototype._remove = function (id) { * @return {Array} removedIds The ids of all removed items */ DataSet.prototype.clear = function (senderId) { - var ids = Object.keys(this.data); + var ids = Object.keys(this.data); - this.data = {}; - this.internalIds = {}; + this.data = {}; + this.internalIds = {}; - this._trigger('remove', {items: ids}, senderId); + this._trigger('remove', {items: ids}, senderId); - return ids; + return ids; }; /** @@ -5624,22 +5085,22 @@ DataSet.prototype.clear = function (senderId) { * @return {Object | null} item Item containing max value, or null if no items */ DataSet.prototype.max = function (field) { - var data = this.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; - } - } + var data = this.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; + } } + } - return max; + return max; }; /** @@ -5648,22 +5109,22 @@ DataSet.prototype.max = function (field) { * @return {Object | null} item Item containing max value, or null if no items */ DataSet.prototype.min = function (field) { - var data = this.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; - } - } + var data = this.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; + } } + } - return min; + return min; }; /** @@ -5675,30 +5136,30 @@ DataSet.prototype.min = function (field) { * The returned array is unordered. */ DataSet.prototype.distinct = function (field) { - var data = this.data, - values = [], - fieldType = this.options.convert[field], - count = 0; - - for (var prop in data) { - if (data.hasOwnProperty(prop)) { - var item = data[prop]; - var value = util.convert(item[field], fieldType); - var exists = false; - for (var i = 0; i < count; i++) { - if (values[i] == value) { - exists = true; - break; - } - } - if (!exists) { - values[count] = value; - count++; - } + var data = this.data, + values = [], + fieldType = this.options.convert[field], + count = 0; + + for (var prop in data) { + if (data.hasOwnProperty(prop)) { + var item = data[prop]; + var value = util.convert(item[field], fieldType); + var exists = false; + for (var i = 0; i < count; i++) { + if (values[i] == value) { + exists = true; + break; } + } + if (!exists) { + values[count] = value; + count++; + } } + } - return values; + return values; }; /** @@ -5708,32 +5169,32 @@ DataSet.prototype.distinct = function (field) { * @private */ DataSet.prototype._addItem = function (item) { - var id = item[this.fieldId]; + var id = item[this.fieldId]; - if (id != undefined) { - // check whether this id is already taken - if (this.data[id]) { - // item already exists - throw new Error('Cannot add item: item with id ' + id + ' already exists'); - } - } - else { - // generate an id - id = util.randomUUID(); - item[this.fieldId] = id; - this.internalIds[id] = item; + if (id != undefined) { + // check whether this id is already taken + if (this.data[id]) { + // item already exists + throw new Error('Cannot add item: item with id ' + id + ' already exists'); } + } + else { + // generate an id + id = util.randomUUID(); + item[this.fieldId] = id; + this.internalIds[id] = item; + } - var d = {}; - for (var field in item) { - if (item.hasOwnProperty(field)) { - var fieldType = this.convert[field]; // type may be undefined - d[field] = util.convert(item[field], fieldType); - } + var d = {}; + for (var field in item) { + if (item.hasOwnProperty(field)) { + var fieldType = this.convert[field]; // type may be undefined + d[field] = util.convert(item[field], fieldType); } - this.data[id] = d; + } + this.data[id] = d; - return id; + return id; }; /** @@ -5744,43 +5205,43 @@ DataSet.prototype._addItem = function (item) { * @private */ DataSet.prototype._getItem = function (id, convert) { - var field, value; - - // get the item from the dataset - var raw = this.data[id]; - if (!raw) { - return null; - } - - // convert the items field types - var converted = {}, - fieldId = this.fieldId, - internalIds = this.internalIds; - if (convert) { - for (field in raw) { - if (raw.hasOwnProperty(field)) { - value = raw[field]; - // output all fields, except internal ids - if ((field != fieldId) || !(value in internalIds)) { - converted[field] = util.convert(value, convert[field]); - } - } + var field, value; + + // get the item from the dataset + var raw = this.data[id]; + if (!raw) { + return null; + } + + // convert the items field types + var converted = {}, + fieldId = this.fieldId, + internalIds = this.internalIds; + if (convert) { + for (field in raw) { + if (raw.hasOwnProperty(field)) { + value = raw[field]; + // output all fields, except internal ids + if ((field != fieldId) || !(value in internalIds)) { + converted[field] = util.convert(value, convert[field]); } + } } - else { - // no field types specified, no converting needed - for (field in raw) { - if (raw.hasOwnProperty(field)) { - value = raw[field]; - // output all fields, except internal ids - if ((field != fieldId) || !(value in internalIds)) { - converted[field] = value; - } - } + } + else { + // no field types specified, no converting needed + for (field in raw) { + if (raw.hasOwnProperty(field)) { + value = raw[field]; + // output all fields, except internal ids + if ((field != fieldId) || !(value in internalIds)) { + converted[field] = value; } + } } + } - return converted; + return converted; }; /** @@ -5792,25 +5253,25 @@ DataSet.prototype._getItem = function (id, convert) { * @private */ DataSet.prototype._updateItem = function (item) { - var id = item[this.fieldId]; - if (id == undefined) { - throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')'); - } - var d = this.data[id]; - if (!d) { - // item doesn't exist - throw new Error('Cannot update item: no item with id ' + id + ' found'); - } + var id = item[this.fieldId]; + if (id == undefined) { + throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')'); + } + var d = this.data[id]; + if (!d) { + // item doesn't exist + throw new Error('Cannot update item: no item with id ' + id + ' found'); + } - // merge with current item - for (var field in item) { - if (item.hasOwnProperty(field)) { - var fieldType = this.convert[field]; // type may be undefined - d[field] = util.convert(item[field], fieldType); - } + // merge with current item + for (var field in item) { + if (item.hasOwnProperty(field)) { + var fieldType = this.convert[field]; // type may be undefined + d[field] = util.convert(item[field], fieldType); } + } - return id; + return id; }; /** @@ -5820,11 +5281,11 @@ DataSet.prototype._updateItem = function (item) { * @private */ DataSet.prototype._getColumnNames = function (dataTable) { - var columns = []; - for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) { - columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col); - } - return columns; + var columns = []; + for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) { + columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col); + } + return columns; }; /** @@ -5835,12 +5296,12 @@ DataSet.prototype._getColumnNames = function (dataTable) { * @private */ DataSet.prototype._appendRow = function (dataTable, columns, item) { - var row = dataTable.addRow(); + var row = dataTable.addRow(); - for (var col = 0, cols = columns.length; col < cols; col++) { - var field = columns[col]; - dataTable.setValue(row, col, item[field]); - } + for (var col = 0, cols = columns.length; col < cols; col++) { + var field = columns[col]; + dataTable.setValue(row, col, item[field]); + } }; /** @@ -5854,67 +5315,70 @@ DataSet.prototype._appendRow = function (dataTable, columns, item) { * @constructor DataView */ function DataView (data, options) { - this.id = util.randomUUID(); + this.id = util.randomUUID(); - this.data = null; - this.ids = {}; // ids of the items currently in memory (just contains a boolean true) - this.options = options || {}; - this.fieldId = 'id'; // name of the field containing id - this.subscribers = {}; // event subscribers + this.data = null; + this.ids = {}; // ids of the items currently in memory (just contains a boolean true) + this.options = options || {}; + this.fieldId = 'id'; // name of the field containing id + this.subscribers = {}; // event subscribers - var me = this; - this.listener = function () { - me._onEvent.apply(me, arguments); - }; + var me = this; + this.listener = function () { + me._onEvent.apply(me, arguments); + }; - this.setData(data); + this.setData(data); } +// TODO: implement a function .config() to dynamically update things like configured filter +// and trigger changes accordingly + /** * Set a data source for the view * @param {DataSet | DataView} data */ DataView.prototype.setData = function (data) { - var ids, dataItems, i, len; + var ids, dataItems, i, len; - if (this.data) { - // unsubscribe from current dataset - if (this.data.unsubscribe) { - this.data.unsubscribe('*', this.listener); - } + if (this.data) { + // unsubscribe from current dataset + if (this.data.unsubscribe) { + this.data.unsubscribe('*', this.listener); + } - // trigger a remove of all items in memory - ids = []; - for (var id in this.ids) { - if (this.ids.hasOwnProperty(id)) { - ids.push(id); - } - } - this.ids = {}; - this._trigger('remove', {items: ids}); + // trigger a remove of all items in memory + ids = []; + for (var id in this.ids) { + if (this.ids.hasOwnProperty(id)) { + ids.push(id); + } } + this.ids = {}; + this._trigger('remove', {items: ids}); + } - this.data = data; + this.data = data; - if (this.data) { - // update fieldId - this.fieldId = this.options.fieldId || - (this.data && this.data.options && this.data.options.fieldId) || - 'id'; + if (this.data) { + // update fieldId + this.fieldId = this.options.fieldId || + (this.data && this.data.options && this.data.options.fieldId) || + 'id'; - // trigger an add of all added items - ids = this.data.getIds({filter: this.options && this.options.filter}); - for (i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - this.ids[id] = true; - } - this._trigger('add', {items: ids}); + // trigger an add of all added items + ids = this.data.getIds({filter: this.options && this.options.filter}); + for (i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + this.ids[id] = true; + } + this._trigger('add', {items: ids}); - // subscribe to new dataset - if (this.data.subscribe) { - this.data.subscribe('*', this.listener); - } + // subscribe to new dataset + if (this.data.subscribe) { + this.data.subscribe('*', this.listener); } + } }; /** @@ -5952,42 +5416,42 @@ DataView.prototype.setData = function (data) { * @param args */ DataView.prototype.get = function (args) { - var me = this; - - // parse the arguments - var ids, options, data; - var firstType = util.getType(arguments[0]); - if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') { - // get(id(s) [, options] [, data]) - ids = arguments[0]; // can be a single id or an array with ids - options = arguments[1]; - data = arguments[2]; - } - else { - // get([, options] [, data]) - options = arguments[0]; - data = arguments[1]; - } + var me = this; + + // parse the arguments + var ids, options, data; + var firstType = util.getType(arguments[0]); + if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') { + // get(id(s) [, options] [, data]) + ids = arguments[0]; // can be a single id or an array with ids + options = arguments[1]; + data = arguments[2]; + } + else { + // get([, options] [, data]) + options = arguments[0]; + data = arguments[1]; + } - // extend the options with the default options and provided options - var viewOptions = util.extend({}, this.options, options); + // extend the options with the default options and provided options + var viewOptions = util.extend({}, this.options, options); - // create a combined filter method when needed - if (this.options.filter && options && options.filter) { - viewOptions.filter = function (item) { - return me.options.filter(item) && options.filter(item); - } + // create a combined filter method when needed + if (this.options.filter && options && options.filter) { + viewOptions.filter = function (item) { + return me.options.filter(item) && options.filter(item); } + } - // build up the call to the linked data set - var getArguments = []; - if (ids != undefined) { - getArguments.push(ids); - } - getArguments.push(viewOptions); - getArguments.push(data); + // build up the call to the linked data set + var getArguments = []; + if (ids != undefined) { + getArguments.push(ids); + } + getArguments.push(viewOptions); + getArguments.push(data); - return this.data && this.data.get.apply(this.data, getArguments); + return this.data && this.data.get.apply(this.data, getArguments); }; /** @@ -5999,36 +5463,36 @@ DataView.prototype.get = function (args) { * @return {Array} ids */ DataView.prototype.getIds = function (options) { - var ids; + var ids; - if (this.data) { - var defaultFilter = this.options.filter; - var filter; + 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; - } + if (options && options.filter) { + if (defaultFilter) { + filter = function (item) { + return defaultFilter(item) && options.filter(item); } - else { - filter = defaultFilter; - } - - ids = this.data.getIds({ - filter: filter, - order: options && options.order - }); + } + else { + filter = options.filter; + } } else { - ids = []; + filter = defaultFilter; } - return ids; + ids = this.data.getIds({ + filter: filter, + order: options && options.order + }); + } + else { + ids = []; + } + + return ids; }; /** @@ -6041,80 +5505,80 @@ DataView.prototype.getIds = function (options) { * @private */ DataView.prototype._onEvent = function (event, params, senderId) { - var i, len, id, item, - ids = params && params.items, - data = this.data, - added = [], - updated = [], - removed = []; - - if (ids && data) { - switch (event) { - case 'add': - // filter the ids of the added items - for (i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - item = this.get(id); - if (item) { - this.ids[id] = true; - added.push(id); - } - } + var i, len, id, item, + ids = params && params.items, + data = this.data, + added = [], + updated = [], + removed = []; + + if (ids && data) { + switch (event) { + case 'add': + // filter the ids of the added items + for (i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + item = this.get(id); + if (item) { + this.ids[id] = true; + added.push(id); + } + } - break; + break; - case 'update': - // determine the event from the views viewpoint: an updated - // item can be added, updated, or removed from this view. - for (i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - item = this.get(id); - - if (item) { - if (this.ids[id]) { - updated.push(id); - } - else { - this.ids[id] = true; - added.push(id); - } - } - else { - if (this.ids[id]) { - delete this.ids[id]; - removed.push(id); - } - else { - // nothing interesting for me :-( - } - } - } + case 'update': + // determine the event from the views viewpoint: an updated + // item can be added, updated, or removed from this view. + for (i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + item = this.get(id); - break; + if (item) { + if (this.ids[id]) { + updated.push(id); + } + else { + this.ids[id] = true; + added.push(id); + } + } + else { + if (this.ids[id]) { + delete this.ids[id]; + removed.push(id); + } + else { + // nothing interesting for me :-( + } + } + } - case 'remove': - // filter the ids of the removed items - for (i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - if (this.ids[id]) { - delete this.ids[id]; - removed.push(id); - } - } + break; - break; + case 'remove': + // filter the ids of the removed items + for (i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + if (this.ids[id]) { + delete this.ids[id]; + removed.push(id); + } } - if (added.length) { - this._trigger('add', {items: added}, senderId); - } - if (updated.length) { - this._trigger('update', {items: updated}, senderId); - } - if (removed.length) { - this._trigger('remove', {items: removed}, senderId); - } + break; + } + + if (added.length) { + this._trigger('add', {items: added}, senderId); + } + if (updated.length) { + this._trigger('update', {items: updated}, senderId); } + if (removed.length) { + this._trigger('remove', {items: removed}, senderId); + } + } }; // copy subscription functionality from DataSet @@ -6149,29 +5613,29 @@ DataView.prototype._trigger = DataSet.prototype._trigger; * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds */ TimeStep = function(start, end, minimumStep) { - // variables - this.current = new Date(); - this._start = new Date(); - this._end = new Date(); + // variables + this.current = new Date(); + this._start = new Date(); + this._end = new Date(); - this.autoScale = true; - this.scale = TimeStep.SCALE.DAY; - this.step = 1; + this.autoScale = true; + this.scale = TimeStep.SCALE.DAY; + this.step = 1; - // initialize the range - this.setRange(start, end, minimumStep); + // initialize the range + this.setRange(start, end, minimumStep); }; /// enum scale TimeStep.SCALE = { - MILLISECOND: 1, - SECOND: 2, - MINUTE: 3, - HOUR: 4, - DAY: 5, - WEEKDAY: 6, - MONTH: 7, - YEAR: 8 + MILLISECOND: 1, + SECOND: 2, + MINUTE: 3, + HOUR: 4, + DAY: 5, + WEEKDAY: 6, + MONTH: 7, + YEAR: 8 }; @@ -6186,24 +5650,24 @@ TimeStep.SCALE = { * @param {int} [minimumStep] Optional. Minimum step size in milliseconds */ TimeStep.prototype.setRange = function(start, end, minimumStep) { - if (!(start instanceof Date) || !(end instanceof Date)) { - throw "No legal start or end date in method setRange"; - } + if (!(start instanceof Date) || !(end instanceof Date)) { + throw "No legal start or end date in method setRange"; + } - this._start = (start != undefined) ? new Date(start.valueOf()) : new Date(); - this._end = (end != undefined) ? new Date(end.valueOf()) : new Date(); + this._start = (start != undefined) ? new Date(start.valueOf()) : new Date(); + this._end = (end != undefined) ? new Date(end.valueOf()) : new Date(); - if (this.autoScale) { - this.setMinimumStep(minimumStep); - } + if (this.autoScale) { + this.setMinimumStep(minimumStep); + } }; /** * Set the range iterator to the start date. */ TimeStep.prototype.first = function() { - this.current = new Date(this._start.valueOf()); - this.roundToMinor(); + this.current = new Date(this._start.valueOf()); + this.roundToMinor(); }; /** @@ -6211,36 +5675,36 @@ TimeStep.prototype.first = function() { * This must be executed once when the current date is set to start Date */ TimeStep.prototype.roundToMinor = function() { - // round to floor - // IMPORTANT: we have no breaks in this switch! (this is no bug) - //noinspection FallthroughInSwitchStatementJS - switch (this.scale) { - case TimeStep.SCALE.YEAR: - this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step)); - this.current.setMonth(0); - case TimeStep.SCALE.MONTH: this.current.setDate(1); - case TimeStep.SCALE.DAY: // intentional fall through - case TimeStep.SCALE.WEEKDAY: this.current.setHours(0); - case TimeStep.SCALE.HOUR: this.current.setMinutes(0); - case TimeStep.SCALE.MINUTE: this.current.setSeconds(0); - case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0); - //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds - } + // round to floor + // IMPORTANT: we have no breaks in this switch! (this is no bug) + //noinspection FallthroughInSwitchStatementJS + switch (this.scale) { + case TimeStep.SCALE.YEAR: + this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step)); + this.current.setMonth(0); + case TimeStep.SCALE.MONTH: this.current.setDate(1); + case TimeStep.SCALE.DAY: // intentional fall through + case TimeStep.SCALE.WEEKDAY: this.current.setHours(0); + case TimeStep.SCALE.HOUR: this.current.setMinutes(0); + case TimeStep.SCALE.MINUTE: this.current.setSeconds(0); + case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0); + //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds + } - if (this.step != 1) { - // round down to the first minor value that is a multiple of the current step size - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break; - case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break; - case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break; - case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break; - case TimeStep.SCALE.WEEKDAY: // intentional fall through - case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break; - case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break; - case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break; - default: break; - } + if (this.step != 1) { + // round down to the first minor value that is a multiple of the current step size + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break; + case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break; + case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break; + case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break; + case TimeStep.SCALE.WEEKDAY: // intentional fall through + case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break; + case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break; + case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break; + default: break; } + } }; /** @@ -6248,70 +5712,70 @@ TimeStep.prototype.roundToMinor = function() { * @return {boolean} true if the current date has not passed the end date */ TimeStep.prototype.hasNext = function () { - return (this.current.valueOf() <= this._end.valueOf()); + return (this.current.valueOf() <= this._end.valueOf()); }; /** * Do the next step */ TimeStep.prototype.next = function() { - var prev = this.current.valueOf(); - - // Two cases, needed to prevent issues with switching daylight savings - // (end of March and end of October) - if (this.current.getMonth() < 6) { - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND: - - this.current = new Date(this.current.valueOf() + this.step); break; - case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break; - case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break; - case TimeStep.SCALE.HOUR: - this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60); - // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...) - var h = this.current.getHours(); - this.current.setHours(h - (h % this.step)); - break; - case TimeStep.SCALE.WEEKDAY: // intentional fall through - case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; - case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; - case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break; - default: break; - } + var prev = this.current.valueOf(); + + // Two cases, needed to prevent issues with switching daylight savings + // (end of March and end of October) + if (this.current.getMonth() < 6) { + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND: + + this.current = new Date(this.current.valueOf() + this.step); break; + case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break; + case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break; + case TimeStep.SCALE.HOUR: + this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60); + // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...) + var h = this.current.getHours(); + this.current.setHours(h - (h % this.step)); + break; + case TimeStep.SCALE.WEEKDAY: // intentional fall through + case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; + case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; + case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break; + default: break; } - else { - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break; - case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break; - case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break; - case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break; - case TimeStep.SCALE.WEEKDAY: // intentional fall through - case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; - case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; - case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break; - default: break; - } + } + else { + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break; + case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break; + case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break; + case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break; + case TimeStep.SCALE.WEEKDAY: // intentional fall through + case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; + case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; + case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break; + default: break; } + } - if (this.step != 1) { - // round down to the correct major value - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break; - case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break; - case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break; - case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break; - case TimeStep.SCALE.WEEKDAY: // intentional fall through - case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break; - case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break; - case TimeStep.SCALE.YEAR: break; // nothing to do for year - default: break; - } + if (this.step != 1) { + // round down to the correct major value + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break; + case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break; + case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break; + case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break; + case TimeStep.SCALE.WEEKDAY: // intentional fall through + case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break; + case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break; + case TimeStep.SCALE.YEAR: break; // nothing to do for year + default: break; } + } - // safety mechanism: if current time is still unchanged, move to the end - if (this.current.valueOf() == prev) { - this.current = new Date(this._end.valueOf()); - } + // safety mechanism: if current time is still unchanged, move to the end + if (this.current.valueOf() == prev) { + this.current = new Date(this._end.valueOf()); + } }; @@ -6320,7 +5784,7 @@ TimeStep.prototype.next = function() { * @return {Date} current The current date */ TimeStep.prototype.getCurrent = function() { - return this.current; + return this.current; }; /** @@ -6337,13 +5801,13 @@ TimeStep.prototype.getCurrent = function() { * example 1, 2, 5, or 10. */ TimeStep.prototype.setScale = function(newScale, newStep) { - this.scale = newScale; + this.scale = newScale; - if (newStep > 0) { - this.step = newStep; - } + if (newStep > 0) { + this.step = newStep; + } - this.autoScale = false; + this.autoScale = false; }; /** @@ -6351,7 +5815,7 @@ TimeStep.prototype.setScale = function(newScale, newStep) { * @param {boolean} enable If true, autoascaling is set true */ TimeStep.prototype.setAutoScale = function (enable) { - this.autoScale = enable; + this.autoScale = enable; }; @@ -6360,48 +5824,48 @@ TimeStep.prototype.setAutoScale = function (enable) { * @param {Number} [minimumStep] The minimum step size in milliseconds */ TimeStep.prototype.setMinimumStep = function(minimumStep) { - if (minimumStep == undefined) { - return; - } + if (minimumStep == undefined) { + return; + } - var stepYear = (1000 * 60 * 60 * 24 * 30 * 12); - var stepMonth = (1000 * 60 * 60 * 24 * 30); - var stepDay = (1000 * 60 * 60 * 24); - var stepHour = (1000 * 60 * 60); - var stepMinute = (1000 * 60); - var stepSecond = (1000); - var stepMillisecond= (1); - - // find the smallest step that is larger than the provided minimumStep - if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;} - if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;} - if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;} - if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;} - if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;} - if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;} - if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;} - if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;} - if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;} - if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;} - if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;} - if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;} - if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;} - if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;} - if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;} - if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;} - if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;} - if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;} - if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;} - if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;} - if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;} - if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;} - if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;} - if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;} - if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;} - if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;} - if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;} - if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;} - if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;} + var stepYear = (1000 * 60 * 60 * 24 * 30 * 12); + var stepMonth = (1000 * 60 * 60 * 24 * 30); + var stepDay = (1000 * 60 * 60 * 24); + var stepHour = (1000 * 60 * 60); + var stepMinute = (1000 * 60); + var stepSecond = (1000); + var stepMillisecond= (1); + + // find the smallest step that is larger than the provided minimumStep + if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;} + if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;} + if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;} + if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;} + if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;} + if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;} + if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;} + if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;} + if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;} + if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;} + if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;} + if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;} + if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;} + if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;} + if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;} + if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;} + if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;} + if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;} + if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;} + if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;} + if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;} + if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;} + if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;} + if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;} + if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;} + if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;} + if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;} + if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;} + if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;} }; /** @@ -6410,87 +5874,87 @@ TimeStep.prototype.setMinimumStep = function(minimumStep) { * @param {Date} date the date to be snapped */ TimeStep.prototype.snap = function(date) { - if (this.scale == TimeStep.SCALE.YEAR) { - var year = date.getFullYear() + Math.round(date.getMonth() / 12); - date.setFullYear(Math.round(year / this.step) * this.step); - date.setMonth(0); - date.setDate(0); - date.setHours(0); - date.setMinutes(0); - date.setSeconds(0); - date.setMilliseconds(0); + if (this.scale == TimeStep.SCALE.YEAR) { + var year = date.getFullYear() + Math.round(date.getMonth() / 12); + date.setFullYear(Math.round(year / this.step) * this.step); + date.setMonth(0); + date.setDate(0); + date.setHours(0); + date.setMinutes(0); + date.setSeconds(0); + date.setMilliseconds(0); + } + else if (this.scale == TimeStep.SCALE.MONTH) { + if (date.getDate() > 15) { + date.setDate(1); + date.setMonth(date.getMonth() + 1); + // important: first set Date to 1, after that change the month. } - else if (this.scale == TimeStep.SCALE.MONTH) { - if (date.getDate() > 15) { - date.setDate(1); - date.setMonth(date.getMonth() + 1); - // important: first set Date to 1, after that change the month. - } - else { - date.setDate(1); - } - - date.setHours(0); - date.setMinutes(0); - date.setSeconds(0); - date.setMilliseconds(0); + else { + date.setDate(1); } - else if (this.scale == TimeStep.SCALE.DAY || - this.scale == TimeStep.SCALE.WEEKDAY) { - //noinspection FallthroughInSwitchStatementJS - switch (this.step) { - case 5: - case 2: - date.setHours(Math.round(date.getHours() / 24) * 24); break; - default: - date.setHours(Math.round(date.getHours() / 12) * 12); break; - } - date.setMinutes(0); + + date.setHours(0); + date.setMinutes(0); + date.setSeconds(0); + date.setMilliseconds(0); + } + else if (this.scale == TimeStep.SCALE.DAY || + this.scale == TimeStep.SCALE.WEEKDAY) { + //noinspection FallthroughInSwitchStatementJS + switch (this.step) { + case 5: + case 2: + date.setHours(Math.round(date.getHours() / 24) * 24); break; + default: + date.setHours(Math.round(date.getHours() / 12) * 12); break; + } + date.setMinutes(0); + date.setSeconds(0); + date.setMilliseconds(0); + } + else if (this.scale == TimeStep.SCALE.HOUR) { + switch (this.step) { + case 4: + date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break; + default: + date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break; + } + date.setSeconds(0); + date.setMilliseconds(0); + } else if (this.scale == TimeStep.SCALE.MINUTE) { + //noinspection FallthroughInSwitchStatementJS + switch (this.step) { + case 15: + case 10: + date.setMinutes(Math.round(date.getMinutes() / 5) * 5); date.setSeconds(0); - date.setMilliseconds(0); + break; + case 5: + date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break; + default: + date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break; } - else if (this.scale == TimeStep.SCALE.HOUR) { - switch (this.step) { - case 4: - date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break; - default: - date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break; - } - date.setSeconds(0); - date.setMilliseconds(0); - } else if (this.scale == TimeStep.SCALE.MINUTE) { - //noinspection FallthroughInSwitchStatementJS - switch (this.step) { - case 15: - case 10: - date.setMinutes(Math.round(date.getMinutes() / 5) * 5); - date.setSeconds(0); - break; - case 5: - date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break; - default: - date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break; - } + date.setMilliseconds(0); + } + else if (this.scale == TimeStep.SCALE.SECOND) { + //noinspection FallthroughInSwitchStatementJS + switch (this.step) { + case 15: + case 10: + date.setSeconds(Math.round(date.getSeconds() / 5) * 5); date.setMilliseconds(0); + break; + case 5: + date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break; + default: + date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break; } - else if (this.scale == TimeStep.SCALE.SECOND) { - //noinspection FallthroughInSwitchStatementJS - switch (this.step) { - case 15: - case 10: - date.setSeconds(Math.round(date.getSeconds() / 5) * 5); - date.setMilliseconds(0); - break; - case 5: - date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break; - default: - date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break; - } - } - else if (this.scale == TimeStep.SCALE.MILLISECOND) { - var step = this.step > 5 ? this.step / 2 : 1; - date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step); - } + } + else if (this.scale == TimeStep.SCALE.MILLISECOND) { + var step = this.step > 5 ? this.step / 2 : 1; + date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step); + } }; /** @@ -6499,26 +5963,26 @@ TimeStep.prototype.snap = function(date) { * @return {boolean} true if current date is major, else false. */ TimeStep.prototype.isMajor = function() { - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND: - return (this.current.getMilliseconds() == 0); - case TimeStep.SCALE.SECOND: - return (this.current.getSeconds() == 0); - case TimeStep.SCALE.MINUTE: - return (this.current.getHours() == 0) && (this.current.getMinutes() == 0); - // Note: this is no bug. Major label is equal for both minute and hour scale - case TimeStep.SCALE.HOUR: - return (this.current.getHours() == 0); - case TimeStep.SCALE.WEEKDAY: // intentional fall through - case TimeStep.SCALE.DAY: - return (this.current.getDate() == 1); - case TimeStep.SCALE.MONTH: - return (this.current.getMonth() == 0); - case TimeStep.SCALE.YEAR: - return false; - default: - return false; - } + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND: + return (this.current.getMilliseconds() == 0); + case TimeStep.SCALE.SECOND: + return (this.current.getSeconds() == 0); + case TimeStep.SCALE.MINUTE: + return (this.current.getHours() == 0) && (this.current.getMinutes() == 0); + // Note: this is no bug. Major label is equal for both minute and hour scale + case TimeStep.SCALE.HOUR: + return (this.current.getHours() == 0); + case TimeStep.SCALE.WEEKDAY: // intentional fall through + case TimeStep.SCALE.DAY: + return (this.current.getDate() == 1); + case TimeStep.SCALE.MONTH: + return (this.current.getMonth() == 0); + case TimeStep.SCALE.YEAR: + return false; + default: + return false; + } }; @@ -6529,21 +5993,21 @@ TimeStep.prototype.isMajor = function() { * @param {Date} [date] custom date. if not provided, current date is taken */ TimeStep.prototype.getLabelMinor = function(date) { - if (date == undefined) { - date = this.current; - } + if (date == undefined) { + date = this.current; + } - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS'); - case TimeStep.SCALE.SECOND: return moment(date).format('s'); - case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm'); - case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm'); - case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D'); - case TimeStep.SCALE.DAY: return moment(date).format('D'); - case TimeStep.SCALE.MONTH: return moment(date).format('MMM'); - case TimeStep.SCALE.YEAR: return moment(date).format('YYYY'); - default: return ''; - } + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS'); + case TimeStep.SCALE.SECOND: return moment(date).format('s'); + case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm'); + case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm'); + case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D'); + case TimeStep.SCALE.DAY: return moment(date).format('D'); + case TimeStep.SCALE.MONTH: return moment(date).format('MMM'); + case TimeStep.SCALE.YEAR: return moment(date).format('YYYY'); + default: return ''; + } }; @@ -6554,22 +6018,22 @@ TimeStep.prototype.getLabelMinor = function(date) { * @param {Date} [date] custom date. if not provided, current date is taken */ TimeStep.prototype.getLabelMajor = function(date) { - if (date == undefined) { - date = this.current; - } + if (date == undefined) { + date = this.current; + } - //noinspection FallthroughInSwitchStatementJS - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss'); - case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm'); - case TimeStep.SCALE.MINUTE: - case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM'); - case TimeStep.SCALE.WEEKDAY: - case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY'); - case TimeStep.SCALE.MONTH: return moment(date).format('YYYY'); - case TimeStep.SCALE.YEAR: return ''; - default: return ''; - } + //noinspection FallthroughInSwitchStatementJS + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss'); + case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm'); + case TimeStep.SCALE.MINUTE: + case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM'); + case TimeStep.SCALE.WEEKDAY: + case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY'); + case TimeStep.SCALE.MONTH: return moment(date).format('YYYY'); + case TimeStep.SCALE.YEAR: return ''; + default: return ''; + } }; /** @@ -6579,39 +6043,39 @@ TimeStep.prototype.getLabelMajor = function(date) { * @param {Object} [options] */ function Stack (parent, options) { - this.parent = parent; - - this.options = options || {}; - this.defaultOptions = { - order: function (a, b) { - //return (b.width - a.width) || (a.left - b.left); // TODO: cleanup - // Order: ranges over non-ranges, ranged ordered by width, and - // lastly ordered by start. - if (a instanceof ItemRange) { - if (b instanceof ItemRange) { - var aInt = (a.data.end - a.data.start); - var bInt = (b.data.end - b.data.start); - return (aInt - bInt) || (a.data.start - b.data.start); - } - else { - return -1; - } - } - else { - if (b instanceof ItemRange) { - return 1; - } - else { - return (a.data.start - b.data.start); - } - } - }, - margin: { - item: 10 + this.parent = parent; + + this.options = options || {}; + this.defaultOptions = { + order: function (a, b) { + //return (b.width - a.width) || (a.left - b.left); // TODO: cleanup + // Order: ranges over non-ranges, ranged ordered by width, and + // lastly ordered by start. + if (a instanceof ItemRange) { + if (b instanceof ItemRange) { + var aInt = (a.data.end - a.data.start); + var bInt = (b.data.end - b.data.start); + return (aInt - bInt) || (a.data.start - b.data.start); } - }; + else { + return -1; + } + } + else { + if (b instanceof ItemRange) { + return 1; + } + else { + return (a.data.start - b.data.start); + } + } + }, + margin: { + item: 10 + } + }; - this.ordered = []; // ordered items + this.ordered = []; // ordered items } /** @@ -6622,9 +6086,9 @@ function Stack (parent, options) { * {function} order Stacking order */ Stack.prototype.setOptions = function setOptions (options) { - util.extend(this.options, options); + util.extend(this.options, options); - // TODO: register on data changes at the connected parent itemset, and update the changed part only and immediately + // TODO: register on data changes at the connected parent itemset, and update the changed part only and immediately }; /** @@ -6632,8 +6096,8 @@ Stack.prototype.setOptions = function setOptions (options) { * distance equal to options.margin.item. */ Stack.prototype.update = function update() { - this._order(); - this._stack(); + this._order(); + this._stack(); }; /** @@ -6644,31 +6108,31 @@ Stack.prototype.update = function update() { * @private */ Stack.prototype._order = function _order () { - var items = this.parent.items; - if (!items) { - throw new Error('Cannot stack items: parent does not contain items'); - } - - // TODO: store the sorted items, to have less work later on - var ordered = []; - var index = 0; - // items is a map (no array) - util.forEach(items, function (item) { - if (item.visible) { - ordered[index] = item; - index++; - } - }); + var items = this.parent.items; + if (!items) { + throw new Error('Cannot stack items: parent does not contain items'); + } - //if a customer stack order function exists, use it. - var order = this.options.order || this.defaultOptions.order; - if (!(typeof order === 'function')) { - throw new Error('Option order must be a function'); - } + // TODO: store the sorted items, to have less work later on + var ordered = []; + var index = 0; + // items is a map (no array) + util.forEach(items, function (item) { + if (item.visible) { + ordered[index] = item; + index++; + } + }); + + //if a customer stack order function exists, use it. + var order = this.options.order || this.defaultOptions.order; + if (!(typeof order === 'function')) { + throw new Error('Option order must be a function'); + } - ordered.sort(order); + ordered.sort(order); - this.ordered = ordered; + this.ordered = ordered; }; /** @@ -6677,40 +6141,40 @@ Stack.prototype._order = function _order () { * @private */ Stack.prototype._stack = function _stack () { - var i, - iMax, - ordered = this.ordered, - options = this.options, - orientation = options.orientation || this.defaultOptions.orientation, - axisOnTop = (orientation == 'top'), - margin; + var i, + iMax, + ordered = this.ordered, + options = this.options, + orientation = options.orientation || this.defaultOptions.orientation, + axisOnTop = (orientation == 'top'), + margin; + + if (options.margin && options.margin.item !== undefined) { + margin = options.margin.item; + } + else { + margin = this.defaultOptions.margin.item + } - if (options.margin && options.margin.item !== undefined) { - margin = options.margin.item; - } - else { - margin = this.defaultOptions.margin.item - } - - // calculate new, non-overlapping positions - for (i = 0, iMax = ordered.length; i < iMax; i++) { - var item = ordered[i]; - var collidingItem = null; - do { - // TODO: optimize checking for overlap. when there is a gap without items, - // you only need to check for items from the next item on, not from zero - collidingItem = this.checkOverlap(ordered, i, 0, i - 1, margin); - if (collidingItem != null) { - // There is a collision. Reposition the event above the colliding element - if (axisOnTop) { - item.top = collidingItem.top + collidingItem.height + margin; - } - else { - item.top = collidingItem.top - item.height - margin; - } - } - } while (collidingItem); - } + // calculate new, non-overlapping positions + for (i = 0, iMax = ordered.length; i < iMax; i++) { + var item = ordered[i]; + var collidingItem = null; + do { + // TODO: optimize checking for overlap. when there is a gap without items, + // you only need to check for items from the next item on, not from zero + collidingItem = this.checkOverlap(ordered, i, 0, i - 1, margin); + if (collidingItem != null) { + // There is a collision. Reposition the event above the colliding element + if (axisOnTop) { + item.top = collidingItem.top + collidingItem.height + margin; + } + else { + item.top = collidingItem.top - item.height - margin; + } + } + } while (collidingItem); + } }; /** @@ -6729,21 +6193,21 @@ Stack.prototype._stack = function _stack () { */ Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex, itemStart, itemEnd, margin) { - var collision = this.collision; - - // we loop from end to start, as we suppose that the chance of a - // collision is larger for items at the end, so check these first. - var a = items[itemIndex]; - for (var i = itemEnd; i >= itemStart; i--) { - var b = items[i]; - if (collision(a, b, margin)) { - if (i != itemIndex) { - return b; - } - } + var collision = this.collision; + + // we loop from end to start, as we suppose that the chance of a + // collision is larger for items at the end, so check these first. + var a = items[itemIndex]; + for (var i = itemEnd; i >= itemStart; i--) { + var b = items[i]; + if (collision(a, b, margin)) { + if (i != itemIndex) { + return b; + } } + } - return null; + return null; }; /** @@ -6759,10 +6223,10 @@ Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex, * @return {boolean} true if a and b collide, else false */ Stack.prototype.collision = function collision (a, b, margin) { - return ((a.left - margin) < (b.left + b.getWidth()) && - (a.left + a.getWidth() + margin) > b.left && - (a.top - margin) < (b.top + b.height) && - (a.top + a.height + margin) > b.top); + return ((a.left - margin) < (b.left + b.getWidth()) && + (a.left + a.getWidth() + margin) > b.left && + (a.top - margin) < (b.top + b.height) && + (a.top + a.height + margin) > b.top); }; /** @@ -6774,15 +6238,13 @@ Stack.prototype.collision = function collision (a, b, margin) { * @extends Controller */ function Range(options) { - this.id = util.randomUUID(); - this.start = null; // Number - this.end = null; // Number + this.id = util.randomUUID(); + this.start = null; // Number + this.end = null; // Number - this.options = options || {}; - - this.listeners = []; + this.options = options || {}; - this.setOptions(options); + this.setOptions(options); } /** @@ -6796,14 +6258,25 @@ function Range(options) { * (end - start). */ Range.prototype.setOptions = function (options) { - util.extend(this.options, options); + util.extend(this.options, options); - // re-apply range with new limitations - if (this.start !== null && this.end !== null) { - this.setRange(this.start, this.end); - } + // re-apply range with new limitations + if (this.start !== null && this.end !== null) { + this.setRange(this.start, this.end); + } }; +/** + * Test whether direction has a valid value + * @param {String} direction 'horizontal' or 'vertical' + */ +function validateDirection (direction) { + if (direction != 'horizontal' && direction != 'vertical') { + throw new TypeError('Unknown direction "' + direction + '". ' + + 'Choose "horizontal" or "vertical".'); + } +} + /** * Add listeners for mouse and touch events to the component * @param {Component} component @@ -6811,47 +6284,44 @@ Range.prototype.setOptions = function (options) { * @param {String} direction Available directions: 'horizontal', 'vertical' */ Range.prototype.subscribe = function (component, event, direction) { - var me = this; - var listener; + var me = this; - if (direction != 'horizontal' && direction != 'vertical') { - throw new TypeError('Unknown direction "' + direction + '". ' + - 'Choose "horizontal" or "vertical".'); - } + if (event == 'move') { + // drag start listener + component.on('dragstart', function (event) { + me._onDragStart(event, component); + }); - //noinspection FallthroughInSwitchStatementJS - if (event == 'move') { - listener = { - component: component, - event: event, - direction: direction, - callback: function (event) { - me._onMouseDown(event, listener); - }, - params: {} - }; + // drag listener + component.on('drag', function (event) { + me._onDrag(event, component, direction); + }); - component.on('mousedown', listener.callback); - me.listeners.push(listener); + // drag end listener + component.on('dragend', function (event) { + me._onDragEnd(event, component); + }); + } + else if (event == 'zoom') { + // mouse wheel + function mousewheel (event) { + me._onMouseWheel(event, component, direction); } - else if (event == 'zoom') { - listener = { - component: component, - event: event, - direction: direction, - callback: function (event) { - me._onMouseWheel(event, listener); - }, - params: {} - }; + component.on('mousewheel', mousewheel); + component.on('DOMMouseScroll', mousewheel); // For FF - component.on('mousewheel', listener.callback); - me.listeners.push(listener); - } - else { - throw new TypeError('Unknown event "' + event + '". ' + - 'Choose "move" or "zoom".'); - } + // pinch + component.on('touch', function (event) { + me._onTouch(); + }); + component.on('pinch', function (event) { + me._onPinch(event, component, direction); + }); + } + else { + throw new TypeError('Unknown event "' + event + '". ' + + 'Choose "move" or "zoom".'); + } }; /** @@ -6861,7 +6331,7 @@ Range.prototype.subscribe = function (component, event, direction) { * as parameter. */ Range.prototype.on = function (event, callback) { - events.addListener(this, event, callback); + events.addListener(this, event, callback); }; /** @@ -6871,10 +6341,10 @@ Range.prototype.on = function (event, callback) { * @private */ Range.prototype._trigger = function (event) { - events.trigger(this, event, { - start: this.start, - end: this.end - }); + events.trigger(this, event, { + start: this.start, + end: this.end + }); }; /** @@ -6883,11 +6353,11 @@ Range.prototype._trigger = function (event) { * @param {Number} [end] */ Range.prototype.setRange = function(start, end) { - var changed = this._applyRange(start, end); - if (changed) { - this._trigger('rangechange'); - this._trigger('rangechanged'); - } + var changed = this._applyRange(start, end); + if (changed) { + this._trigger('rangechange'); + this._trigger('rangechanged'); + } }; /** @@ -6900,105 +6370,105 @@ Range.prototype.setRange = function(start, end) { * @private */ Range.prototype._applyRange = function(start, end) { - var newStart = (start != null) ? util.convert(start, 'Number') : this.start, - newEnd = (end != null) ? util.convert(end, 'Number') : this.end, - max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null, - min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null, - diff; + var newStart = (start != null) ? util.convert(start, 'Number') : this.start, + newEnd = (end != null) ? util.convert(end, 'Number') : this.end, + max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null, + min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null, + diff; + + // check for valid number + if (isNaN(newStart) || newStart === null) { + throw new Error('Invalid start "' + start + '"'); + } + if (isNaN(newEnd) || newEnd === null) { + throw new Error('Invalid end "' + end + '"'); + } - // check for valid number - if (isNaN(newStart) || newStart === null) { - throw new Error('Invalid start "' + start + '"'); - } - if (isNaN(newEnd) || newEnd === null) { - throw new Error('Invalid end "' + end + '"'); - } + // prevent start < end + if (newEnd < newStart) { + newEnd = newStart; + } - // prevent start < end - if (newEnd < newStart) { - newEnd = newStart; - } + // prevent start < min + if (min !== null) { + if (newStart < min) { + diff = (min - newStart); + newStart += diff; + newEnd += diff; - // prevent start < min - if (min !== null) { - if (newStart < min) { - diff = (min - newStart); - newStart += diff; - newEnd += diff; - - // prevent end > max - if (max != null) { - if (newEnd > max) { - newEnd = max; - } - } + // prevent end > max + if (max != null) { + if (newEnd > max) { + newEnd = max; } + } } + } - // prevent end > max - if (max !== null) { - if (newEnd > max) { - diff = (newEnd - max); - newStart -= diff; - newEnd -= diff; - - // prevent start < min - if (min != null) { - if (newStart < min) { - newStart = min; - } - } + // prevent end > max + if (max !== null) { + if (newEnd > max) { + diff = (newEnd - max); + newStart -= diff; + newEnd -= diff; + + // prevent start < min + if (min != null) { + if (newStart < min) { + newStart = min; } + } } + } - // prevent (end-start) < zoomMin - if (this.options.zoomMin !== null) { - var zoomMin = parseFloat(this.options.zoomMin); - if (zoomMin < 0) { - zoomMin = 0; - } - if ((newEnd - newStart) < zoomMin) { - if ((this.end - this.start) === zoomMin) { - // ignore this action, we are already zoomed to the minimum - newStart = this.start; - newEnd = this.end; - } - else { - // zoom to the minimum - diff = (zoomMin - (newEnd - newStart)); - newStart -= diff / 2; - newEnd += diff / 2; - } - } + // prevent (end-start) < zoomMin + if (this.options.zoomMin !== null) { + var zoomMin = parseFloat(this.options.zoomMin); + if (zoomMin < 0) { + zoomMin = 0; + } + if ((newEnd - newStart) < zoomMin) { + if ((this.end - this.start) === zoomMin) { + // ignore this action, we are already zoomed to the minimum + newStart = this.start; + newEnd = this.end; + } + else { + // zoom to the minimum + diff = (zoomMin - (newEnd - newStart)); + newStart -= diff / 2; + newEnd += diff / 2; + } } + } - // prevent (end-start) > zoomMax - if (this.options.zoomMax !== null) { - var zoomMax = parseFloat(this.options.zoomMax); - if (zoomMax < 0) { - zoomMax = 0; - } - if ((newEnd - newStart) > zoomMax) { - if ((this.end - this.start) === zoomMax) { - // ignore this action, we are already zoomed to the maximum - newStart = this.start; - newEnd = this.end; - } - else { - // zoom to the maximum - diff = ((newEnd - newStart) - zoomMax); - newStart += diff / 2; - newEnd -= diff / 2; - } - } + // prevent (end-start) > zoomMax + if (this.options.zoomMax !== null) { + var zoomMax = parseFloat(this.options.zoomMax); + if (zoomMax < 0) { + zoomMax = 0; + } + if ((newEnd - newStart) > zoomMax) { + if ((this.end - this.start) === zoomMax) { + // ignore this action, we are already zoomed to the maximum + newStart = this.start; + newEnd = this.end; + } + else { + // zoom to the maximum + diff = ((newEnd - newStart) - zoomMax); + newStart += diff / 2; + newEnd -= diff / 2; + } } + } - var changed = (this.start != newStart || this.end != newEnd); + var changed = (this.start != newStart || this.end != newEnd); - this.start = newStart; - this.end = newEnd; + this.start = newStart; + this.end = newEnd; - return changed; + return changed; }; /** @@ -7006,296 +6476,280 @@ Range.prototype._applyRange = function(start, end) { * @return {Object} An object with start and end properties */ Range.prototype.getRange = function() { - return { - start: this.start, - end: this.end - }; + return { + start: this.start, + end: this.end + }; }; /** - * Calculate the conversion offset and factor for current range, based on + * Calculate the conversion offset and scale for current range, based on * the provided width * @param {Number} width - * @returns {{offset: number, factor: number}} conversion + * @returns {{offset: number, scale: number}} conversion */ Range.prototype.conversion = function (width) { - var start = this.start; - var end = this.end; - - return Range.conversion(this.start, this.end, width); + return Range.conversion(this.start, this.end, width); }; /** - * Static method to calculate the conversion offset and factor for a range, + * Static method to calculate the conversion offset and scale for a range, * based on the provided start, end, and width * @param {Number} start * @param {Number} end * @param {Number} width - * @returns {{offset: number, factor: number}} conversion + * @returns {{offset: number, scale: number}} conversion */ Range.conversion = function (start, end, width) { - if (width != 0 && (end - start != 0)) { - return { - offset: start, - factor: width / (end - start) - } - } - else { - return { - offset: 0, - factor: 1 - }; + if (width != 0 && (end - start != 0)) { + return { + offset: start, + scale: width / (end - start) } + } + else { + return { + offset: 0, + scale: 1 + }; + } }; +// global (private) object to store drag params +var touchParams = {}; + /** - * Start moving horizontally or vertically + * Start dragging horizontally or vertically * @param {Event} event - * @param {Object} listener Listener containing the component and params + * @param {Object} component * @private */ -Range.prototype._onMouseDown = function(event, listener) { - event = event || window.event; - var params = listener.params; - - // only react on left mouse button down - var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1); - if (!leftButtonDown) { - return; - } - - // get mouse position - params.mouseX = util.getPageX(event); - params.mouseY = util.getPageY(event); - params.previousLeft = 0; - params.previousOffset = 0; - - params.moved = false; - params.start = this.start; - params.end = this.end; - - var frame = listener.component.frame; - if (frame) { - frame.style.cursor = 'move'; - } +Range.prototype._onDragStart = function(event, component) { + // refuse to drag when we where pinching to prevent the timeline make a jump + // when releasing the fingers in opposite order from the touch screen + if (touchParams.pinching) return; - // add event listeners to handle moving the contents - // we store the function onmousemove and onmouseup in the timeaxis, - // so we can remove the eventlisteners lateron in the function onmouseup - var me = this; - if (!params.onMouseMove) { - params.onMouseMove = function (event) { - me._onMouseMove(event, listener); - }; - util.addEventListener(document, "mousemove", params.onMouseMove); - } - if (!params.onMouseUp) { - params.onMouseUp = function (event) { - me._onMouseUp(event, listener); - }; - util.addEventListener(document, "mouseup", params.onMouseUp); - } + touchParams.start = this.start; + touchParams.end = this.end; - util.preventDefault(event); + var frame = component.frame; + if (frame) { + frame.style.cursor = 'move'; + } }; /** - * Perform moving operating. - * This function activated from within the funcion TimeAxis._onMouseDown(). + * Perform dragging operating. * @param {Event} event - * @param {Object} listener + * @param {Component} component + * @param {String} direction 'horizontal' or 'vertical' * @private */ -Range.prototype._onMouseMove = function (event, listener) { - event = event || window.event; - - var params = listener.params; - - // calculate change in mouse position - var mouseX = util.getPageX(event); - var mouseY = util.getPageY(event); - - if (params.mouseX == undefined) { - params.mouseX = mouseX; - } - if (params.mouseY == undefined) { - params.mouseY = mouseY; - } +Range.prototype._onDrag = function (event, component, direction) { + validateDirection(direction); - var diffX = mouseX - params.mouseX; - var diffY = mouseY - params.mouseY; - var diff = (listener.direction == 'horizontal') ? diffX : diffY; - - // if mouse movement is big enough, register it as a "moved" event - if (Math.abs(diff) >= 1) { - params.moved = true; - } + // refuse to drag when we where pinching to prevent the timeline make a jump + // when releasing the fingers in opposite order from the touch screen + if (touchParams.pinching) return; - var interval = (params.end - params.start); - var width = (listener.direction == 'horizontal') ? - listener.component.width : listener.component.height; - var diffRange = -diff / width * interval; - this._applyRange(params.start + diffRange, params.end + diffRange); + var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY, + interval = (touchParams.end - touchParams.start), + width = (direction == 'horizontal') ? component.width : component.height, + diffRange = -delta / width * interval; - // fire a rangechange event - this._trigger('rangechange'); + this._applyRange(touchParams.start + diffRange, touchParams.end + diffRange); - util.preventDefault(event); + // fire a rangechange event + this._trigger('rangechange'); }; /** - * Stop moving operating. - * This function activated from within the function Range._onMouseDown(). + * Stop dragging operating. * @param {event} event - * @param {Object} listener + * @param {Component} component * @private */ -Range.prototype._onMouseUp = function (event, listener) { - event = event || window.event; +Range.prototype._onDragEnd = function (event, component) { + // refuse to drag when we where pinching to prevent the timeline make a jump + // when releasing the fingers in opposite order from the touch screen + if (touchParams.pinching) return; - var params = listener.params; - - if (listener.component.frame) { - listener.component.frame.style.cursor = 'auto'; - } - - // remove event listeners here, important for Safari - if (params.onMouseMove) { - util.removeEventListener(document, "mousemove", params.onMouseMove); - params.onMouseMove = null; - } - if (params.onMouseUp) { - util.removeEventListener(document, "mouseup", params.onMouseUp); - params.onMouseUp = null; - } - //util.preventDefault(event); + if (component.frame) { + component.frame.style.cursor = 'auto'; + } - if (params.moved) { - // fire a rangechanged event - this._trigger('rangechanged'); - } + // fire a rangechanged event + this._trigger('rangechanged'); }; /** * Event handler for mouse wheel event, used to zoom * Code from http://adomas.org/javascript-mouse-wheel/ * @param {Event} event - * @param {Object} listener + * @param {Component} component + * @param {String} direction 'horizontal' or 'vertical' * @private */ -Range.prototype._onMouseWheel = function(event, listener) { - event = event || window.event; - - // retrieve delta - var 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) { - var me = this; - var zoom = function () { - // perform the zoom action. Delta is normally 1 or -1 - var zoomFactor = delta / 5.0; - var zoomAround = null; - var frame = listener.component.frame; - if (frame) { - var size, conversion; - if (listener.direction == 'horizontal') { - size = listener.component.width; - conversion = me.conversion(size); - var frameLeft = util.getAbsoluteLeft(frame); - var mouseX = util.getPageX(event); - zoomAround = (mouseX - frameLeft) / conversion.factor + conversion.offset; - } - else { - size = listener.component.height; - conversion = me.conversion(size); - var frameTop = util.getAbsoluteTop(frame); - var mouseY = util.getPageY(event); - zoomAround = ((frameTop + size - mouseY) - frameTop) / conversion.factor + conversion.offset; - } - } +Range.prototype._onMouseWheel = function(event, component, direction) { + validateDirection(direction); + + // retrieve delta + var 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; + } - me.zoom(zoomFactor, zoomAround); - }; + // 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) { + // perform the zoom action. Delta is normally 1 or -1 - zoom(); + // adjust a negative delta such that zooming in with delta 0.1 + // equals zooming out with a delta -0.1 + var scale; + if (delta < 0) { + scale = 1 - (delta / 5); + } + else { + scale = 1 / (1 + (delta / 5)) ; } - // Prevent default actions caused by mouse wheel. - // That might be ugly, but we handle scrolls somehow - // anyway, so don't bother here... - util.preventDefault(event); -}; + // calculate center, the date to zoom around + var gesture = Hammer.event.collectEventData(this, 'scroll', event), + pointer = getPointer(gesture.touches[0], component.frame), + pointerDate = this._pointerToDate(component, direction, pointer); + + this.zoom(scale, pointerDate); + } + // Prevent default actions caused by mouse wheel + // (else the page and timeline both zoom and scroll) + util.preventDefault(event); +}; /** - * Zoom the range the given zoomfactor in or out. Start and end date will - * be adjusted, and the timeline will be redrawn. You can optionally give a - * date around which to zoom. - * For example, try zoomfactor = 0.1 or -0.1 - * @param {Number} zoomFactor Zooming amount. Positive value will zoom in, - * negative value will zoom out - * @param {Number} zoomAround Value around which will be zoomed. Optional + * On start of a touch gesture, initialize scale to 1 + * @private */ -Range.prototype.zoom = function(zoomFactor, zoomAround) { - // if zoomAroundDate is not provided, take it half between start Date and end Date - if (zoomAround == null) { - zoomAround = (this.start + this.end) / 2; - } +Range.prototype._onTouch = function () { + touchParams.start = this.start; + touchParams.end = this.end; + touchParams.pinching = false; + touchParams.center = null; +}; - // prevent zoom factor larger than 1 or smaller than -1 (larger than 1 will - // result in a start>=end ) - if (zoomFactor >= 1) { - zoomFactor = 0.9; - } - if (zoomFactor <= -1) { - zoomFactor = -0.9; - } +/** + * Handle pinch event + * @param {Event} event + * @param {Component} component + * @param {String} direction 'horizontal' or 'vertical' + * @private + */ +Range.prototype._onPinch = function (event, component, direction) { + touchParams.pinching = true; - // adjust a negative factor such that zooming in with 0.1 equals zooming - // out with a factor -0.1 - if (zoomFactor < 0) { - zoomFactor = zoomFactor / (1 + zoomFactor); + if (event.gesture.touches.length > 1) { + if (!touchParams.center) { + touchParams.center = getPointer(event.gesture.center, component.frame); } - // zoom start and end relative to the zoomAround value - var startDiff = (this.start - zoomAround); - var endDiff = (this.end - zoomAround); + var scale = 1 / event.gesture.scale, + initDate = this._pointerToDate(component, direction, touchParams.center), + center = getPointer(event.gesture.center, component.frame), + date = this._pointerToDate(component, direction, center), + delta = date - initDate; // TODO: utilize delta // calculate new start and end - var newStart = this.start - startDiff * zoomFactor; - var newEnd = this.end - endDiff * zoomFactor; + var newStart = parseInt(initDate + (touchParams.start - initDate) * scale); + var newEnd = parseInt(initDate + (touchParams.end - initDate) * scale); + // apply new range this.setRange(newStart, newEnd); + } +}; + +/** + * Helper function to calculate the center date for zooming + * @param {Component} component + * @param {{x: Number, y: Number}} pointer + * @param {String} direction 'horizontal' or 'vertical' + * @return {number} date + * @private + */ +Range.prototype._pointerToDate = function (component, direction, pointer) { + var conversion; + if (direction == 'horizontal') { + var width = component.width; + conversion = this.conversion(width); + return pointer.x / conversion.scale + conversion.offset; + } + else { + var height = component.height; + conversion = this.conversion(height); + return pointer.y / conversion.scale + conversion.offset; + } +}; + +/** + * Get the pointer location relative to the location of the dom element + * @param {{pageX: Number, pageY: Number}} touch + * @param {Element} element HTML DOM element + * @return {{x: Number, y: Number}} pointer + * @private + */ +function getPointer (touch, element) { + return { + x: touch.pageX - vis.util.getAbsoluteLeft(element), + y: touch.pageY - vis.util.getAbsoluteTop(element) + }; +} + +/** + * Zoom the range the given scale in or out. Start and end date will + * be adjusted, and the timeline will be redrawn. You can optionally give a + * date around which to zoom. + * For example, try scale = 0.9 or 1.1 + * @param {Number} scale Scaling factor. Values above 1 will zoom out, + * values below 1 will zoom in. + * @param {Number} [center] Value representing a date around which will + * be zoomed. + */ +Range.prototype.zoom = function(scale, center) { + // if centerDate is not provided, take it half between start Date and end Date + if (center == null) { + center = (this.start + this.end) / 2; + } + + // calculate new start and end + var newStart = center + (this.start - center) * scale; + var newEnd = center + (this.end - center) * scale; + + this.setRange(newStart, newEnd); }; /** - * Move the range with a given factor to the left or right. Start and end - * value will be adjusted. For example, try moveFactor = 0.1 or -0.1 - * @param {Number} moveFactor Moving amount. Positive value will move right, - * negative value will move left + * Move the range with a given delta to the left or right. Start and end + * value will be adjusted. For example, try delta = 0.1 or -0.1 + * @param {Number} delta Moving amount. Positive value will move right, + * negative value will move left */ -Range.prototype.move = function(moveFactor) { - // zoom start Date and end Date relative to the zoomAroundDate - var diff = (this.end - this.start); +Range.prototype.move = function(delta) { + // zoom start Date and end Date relative to the centerDate + var diff = (this.end - this.start); - // apply new values - var newStart = this.start + diff * moveFactor; - var newEnd = this.end + diff * moveFactor; + // apply new values + var newStart = this.start + diff * delta; + var newEnd = this.end + diff * delta; - // TODO: reckon with min and max range + // TODO: reckon with min and max range - this.start = newStart; - this.end = newEnd; + this.start = newStart; + this.end = newEnd; }; /** @@ -7303,16 +6757,16 @@ Range.prototype.move = function(moveFactor) { * @param {Number} moveTo New center point of the range */ Range.prototype.moveTo = function(moveTo) { - var center = (this.start + this.end) / 2; + var center = (this.start + this.end) / 2; - var diff = center - moveTo; + var diff = center - moveTo; - // calculate new start and end - var newStart = this.start - diff; - var newEnd = this.end - diff; + // calculate new start and end + var newStart = this.start - diff; + var newEnd = this.end - diff; - this.setRange(newStart, newEnd); -} + this.setRange(newStart, newEnd); +}; /** * @constructor Controller @@ -7320,11 +6774,11 @@ Range.prototype.moveTo = function(moveTo) { * A Controller controls the reflows and repaints of all visual components */ function Controller () { - this.id = util.randomUUID(); - this.components = {}; + this.id = util.randomUUID(); + this.components = {}; - this.repaintTimer = undefined; - this.reflowTimer = undefined; + this.repaintTimer = undefined; + this.reflowTimer = undefined; } /** @@ -7332,18 +6786,18 @@ function Controller () { * @param {Component} component */ Controller.prototype.add = function add(component) { - // validate the component - if (component.id == undefined) { - throw new Error('Component has no field id'); - } - if (!(component instanceof Component) && !(component instanceof Controller)) { - throw new TypeError('Component must be an instance of ' + - 'prototype Component or Controller'); - } + // validate the component + if (component.id == undefined) { + throw new Error('Component has no field id'); + } + if (!(component instanceof Component) && !(component instanceof Controller)) { + throw new TypeError('Component must be an instance of ' + + 'prototype Component or Controller'); + } - // add the component - component.controller = this; - this.components[component.id] = component; + // add the component + component.controller = this; + this.components[component.id] = component; }; /** @@ -7351,18 +6805,18 @@ Controller.prototype.add = function add(component) { * @param {Component | String} component */ Controller.prototype.remove = function remove(component) { - var id; - for (id in this.components) { - if (this.components.hasOwnProperty(id)) { - if (id == component || this.components[id] == component) { - break; - } - } + var id; + for (id in this.components) { + if (this.components.hasOwnProperty(id)) { + if (id == component || this.components[id] == component) { + break; + } } + } - if (id) { - delete this.components[id]; - } + if (id) { + delete this.components[id]; + } }; /** @@ -7371,18 +6825,18 @@ Controller.prototype.remove = function remove(component) { * is false. */ Controller.prototype.requestReflow = function requestReflow(force) { - if (force) { - this.reflow(); - } - else { - if (!this.reflowTimer) { - var me = this; - this.reflowTimer = setTimeout(function () { - me.reflowTimer = undefined; - me.reflow(); - }, 0); - } + if (force) { + this.reflow(); + } + else { + if (!this.reflowTimer) { + var me = this; + this.reflowTimer = setTimeout(function () { + me.reflowTimer = undefined; + me.reflow(); + }, 0); } + } }; /** @@ -7391,117 +6845,117 @@ Controller.prototype.requestReflow = function requestReflow(force) { * is false. */ Controller.prototype.requestRepaint = function requestRepaint(force) { - if (force) { - this.repaint(); - } - else { - if (!this.repaintTimer) { - var me = this; - this.repaintTimer = setTimeout(function () { - me.repaintTimer = undefined; - me.repaint(); - }, 0); - } + if (force) { + this.repaint(); + } + else { + if (!this.repaintTimer) { + var me = this; + this.repaintTimer = setTimeout(function () { + me.repaintTimer = undefined; + me.repaint(); + }, 0); } + } }; /** * Repaint all components */ Controller.prototype.repaint = function repaint() { - var changed = false; + var changed = false; - // cancel any running repaint request - if (this.repaintTimer) { - clearTimeout(this.repaintTimer); - this.repaintTimer = undefined; - } + // cancel any running repaint request + if (this.repaintTimer) { + clearTimeout(this.repaintTimer); + this.repaintTimer = undefined; + } - var done = {}; + var done = {}; - function repaint(component, id) { - if (!(id in done)) { - // first repaint the components on which this component is dependent - if (component.depends) { - component.depends.forEach(function (dep) { - repaint(dep, dep.id); - }); - } - if (component.parent) { - repaint(component.parent, component.parent.id); - } + function repaint(component, id) { + if (!(id in done)) { + // first repaint the components on which this component is dependent + if (component.depends) { + component.depends.forEach(function (dep) { + repaint(dep, dep.id); + }); + } + if (component.parent) { + repaint(component.parent, component.parent.id); + } - // repaint the component itself and mark as done - changed = component.repaint() || changed; - done[id] = true; - } + // repaint the component itself and mark as done + changed = component.repaint() || changed; + done[id] = true; } + } - util.forEach(this.components, repaint); + util.forEach(this.components, repaint); - // immediately reflow when needed - if (changed) { - this.reflow(); - } - // TODO: limit the number of nested reflows/repaints, prevent loop + // immediately reflow when needed + if (changed) { + this.reflow(); + } + // TODO: limit the number of nested reflows/repaints, prevent loop }; /** * Reflow all components */ Controller.prototype.reflow = function reflow() { - var resized = false; + var resized = false; - // cancel any running repaint request - if (this.reflowTimer) { - clearTimeout(this.reflowTimer); - this.reflowTimer = undefined; - } + // cancel any running repaint request + if (this.reflowTimer) { + clearTimeout(this.reflowTimer); + this.reflowTimer = undefined; + } - var done = {}; + var done = {}; - function reflow(component, id) { - if (!(id in done)) { - // first reflow the components on which this component is dependent - if (component.depends) { - component.depends.forEach(function (dep) { - reflow(dep, dep.id); - }); - } - if (component.parent) { - reflow(component.parent, component.parent.id); - } + function reflow(component, id) { + if (!(id in done)) { + // first reflow the components on which this component is dependent + if (component.depends) { + component.depends.forEach(function (dep) { + reflow(dep, dep.id); + }); + } + if (component.parent) { + reflow(component.parent, component.parent.id); + } - // reflow the component itself and mark as done - resized = component.reflow() || resized; - done[id] = true; - } + // reflow the component itself and mark as done + resized = component.reflow() || resized; + done[id] = true; } + } - util.forEach(this.components, reflow); + util.forEach(this.components, reflow); - // immediately repaint when needed - if (resized) { - this.repaint(); - } - // TODO: limit the number of nested reflows/repaints, prevent loop + // immediately repaint when needed + if (resized) { + this.repaint(); + } + // TODO: limit the number of nested reflows/repaints, prevent loop }; /** * Prototype for visual components */ function Component () { - this.id = null; - this.parent = null; - this.depends = null; - this.controller = null; - this.options = null; - - this.frame = null; // main DOM element - this.top = 0; - this.left = 0; - this.width = 0; - this.height = 0; + this.id = null; + this.parent = null; + this.depends = null; + this.controller = null; + this.options = null; + + this.frame = null; // main DOM element + this.top = 0; + this.left = 0; + this.width = 0; + this.height = 0; } /** @@ -7516,14 +6970,14 @@ function Component () { * {String | Number | function} [height] */ Component.prototype.setOptions = function setOptions(options) { - if (options) { - util.extend(this.options, options); + if (options) { + util.extend(this.options, options); - if (this.controller) { - this.requestRepaint(); - this.requestReflow(); - } + if (this.controller) { + this.requestRepaint(); + this.requestReflow(); } + } }; /** @@ -7534,14 +6988,14 @@ Component.prototype.setOptions = function setOptions(options) { * @return {*} value */ Component.prototype.getOption = function getOption(name) { - var value; - if (this.options) { - value = this.options[name]; - } - if (value === undefined && this.defaultOptions) { - value = this.defaultOptions[name]; - } - return value; + var value; + if (this.options) { + value = this.options[name]; + } + if (value === undefined && this.defaultOptions) { + value = this.defaultOptions[name]; + } + return value; }; /** @@ -7552,8 +7006,8 @@ Component.prototype.getOption = function getOption(name) { */ // TODO: get rid of the getContainer and getFrame methods, provide these via the options Component.prototype.getContainer = function getContainer() { - // should be implemented by the component - return null; + // should be implemented by the component + return null; }; /** @@ -7561,7 +7015,7 @@ Component.prototype.getContainer = function getContainer() { * @returns {HTMLElement | null} frame */ Component.prototype.getFrame = function getFrame() { - return this.frame; + return this.frame; }; /** @@ -7569,8 +7023,8 @@ Component.prototype.getFrame = function getFrame() { * @return {Boolean} changed */ Component.prototype.repaint = function repaint() { - // should be implemented by the component - return false; + // should be implemented by the component + return false; }; /** @@ -7578,8 +7032,8 @@ Component.prototype.repaint = function repaint() { * @return {Boolean} resized */ Component.prototype.reflow = function reflow() { - // should be implemented by the component - return false; + // should be implemented by the component + return false; }; /** @@ -7587,13 +7041,13 @@ Component.prototype.reflow = function reflow() { * @return {Boolean} changed */ Component.prototype.hide = function hide() { - if (this.frame && this.frame.parentNode) { - this.frame.parentNode.removeChild(this.frame); - return true; - } - else { - return false; - } + if (this.frame && this.frame.parentNode) { + this.frame.parentNode.removeChild(this.frame); + return true; + } + else { + return false; + } }; /** @@ -7602,38 +7056,38 @@ Component.prototype.hide = function hide() { * @return {Boolean} changed */ Component.prototype.show = function show() { - if (!this.frame || !this.frame.parentNode) { - return this.repaint(); - } - else { - return false; - } + if (!this.frame || !this.frame.parentNode) { + return this.repaint(); + } + else { + return false; + } }; /** * Request a repaint. The controller will schedule a repaint */ Component.prototype.requestRepaint = function requestRepaint() { - if (this.controller) { - this.controller.requestRepaint(); - } - else { - throw new Error('Cannot request a repaint: no controller configured'); - // TODO: just do a repaint when no parent is configured? - } + if (this.controller) { + this.controller.requestRepaint(); + } + else { + throw new Error('Cannot request a repaint: no controller configured'); + // TODO: just do a repaint when no parent is configured? + } }; /** * Request a reflow. The controller will schedule a reflow */ Component.prototype.requestReflow = function requestReflow() { - if (this.controller) { - this.controller.requestReflow(); - } - else { - throw new Error('Cannot request a reflow: no controller configured'); - // TODO: just do a reflow when no parent is configured? - } + if (this.controller) { + this.controller.requestReflow(); + } + else { + throw new Error('Cannot request a reflow: no controller configured'); + // TODO: just do a reflow when no parent is configured? + } }; /** @@ -7651,11 +7105,11 @@ Component.prototype.requestReflow = function requestReflow() { * @extends Component */ function Panel(parent, depends, options) { - this.id = util.randomUUID(); - this.parent = parent; - this.depends = depends; + this.id = util.randomUUID(); + this.parent = parent; + this.depends = depends; - this.options = options || {}; + this.options = options || {}; } Panel.prototype = new Component(); @@ -7677,7 +7131,7 @@ Panel.prototype.setOptions = Component.prototype.setOptions; * @returns {HTMLElement} container */ Panel.prototype.getContainer = function () { - return this.frame; + return this.frame; }; /** @@ -7685,46 +7139,46 @@ Panel.prototype.getContainer = function () { * @return {Boolean} changed */ Panel.prototype.repaint = function () { - var changed = 0, - update = util.updateProperty, - asSize = util.option.asSize, - options = this.options, - frame = this.frame; - if (!frame) { - frame = document.createElement('div'); - frame.className = 'panel'; - - var className = options.className; - if (className) { - if (typeof className == 'function') { - util.addClassName(frame, String(className())); - } - else { - util.addClassName(frame, String(className)); - } - } + var changed = 0, + update = util.updateProperty, + asSize = util.option.asSize, + options = this.options, + frame = this.frame; + if (!frame) { + frame = document.createElement('div'); + frame.className = 'panel'; + + var className = options.className; + if (className) { + if (typeof className == 'function') { + util.addClassName(frame, String(className())); + } + else { + util.addClassName(frame, String(className)); + } + } - this.frame = frame; - changed += 1; + this.frame = frame; + changed += 1; + } + if (!frame.parentNode) { + if (!this.parent) { + throw new Error('Cannot repaint panel: no parent attached'); } - if (!frame.parentNode) { - if (!this.parent) { - throw new Error('Cannot repaint panel: no parent attached'); - } - var parentContainer = this.parent.getContainer(); - if (!parentContainer) { - throw new Error('Cannot repaint panel: parent has no container element'); - } - parentContainer.appendChild(frame); - changed += 1; + var parentContainer = this.parent.getContainer(); + if (!parentContainer) { + throw new Error('Cannot repaint panel: parent has no container element'); } + parentContainer.appendChild(frame); + changed += 1; + } - changed += update(frame.style, 'top', asSize(options.top, '0px')); - changed += update(frame.style, 'left', asSize(options.left, '0px')); - changed += update(frame.style, 'width', asSize(options.width, '100%')); - changed += update(frame.style, 'height', asSize(options.height, '100%')); + changed += update(frame.style, 'top', asSize(options.top, '0px')); + changed += update(frame.style, 'left', asSize(options.left, '0px')); + changed += update(frame.style, 'width', asSize(options.width, '100%')); + changed += update(frame.style, 'height', asSize(options.height, '100%')); - return (changed > 0); + return (changed > 0); }; /** @@ -7732,21 +7186,21 @@ Panel.prototype.repaint = function () { * @return {Boolean} resized */ Panel.prototype.reflow = function () { - var changed = 0, - update = util.updateProperty, - frame = this.frame; - - if (frame) { - changed += update(this, 'top', frame.offsetTop); - changed += update(this, 'left', frame.offsetLeft); - changed += update(this, 'width', frame.offsetWidth); - changed += update(this, 'height', frame.offsetHeight); - } - else { - changed += 1; - } + var changed = 0, + update = util.updateProperty, + frame = this.frame; + + if (frame) { + changed += update(this, 'top', frame.offsetTop); + changed += update(this, 'left', frame.offsetLeft); + changed += update(this, 'width', frame.offsetWidth); + changed += update(this, 'height', frame.offsetHeight); + } + else { + changed += 1; + } - return (changed > 0); + return (changed > 0); }; /** @@ -7758,15 +7212,15 @@ Panel.prototype.reflow = function () { * @extends Panel */ function RootPanel(container, options) { - this.id = util.randomUUID(); - this.container = container; + this.id = util.randomUUID(); + this.container = container; - this.options = options || {}; - this.defaultOptions = { - autoResize: true - }; + this.options = options || {}; + this.defaultOptions = { + autoResize: true + }; - this.listeners = {}; // event listeners + this.listeners = {}; // event listeners } RootPanel.prototype = new Panel(); @@ -7788,42 +7242,42 @@ RootPanel.prototype.setOptions = Component.prototype.setOptions; * @return {Boolean} changed */ RootPanel.prototype.repaint = function () { - var changed = 0, - update = util.updateProperty, - asSize = util.option.asSize, - options = this.options, - frame = this.frame; + var changed = 0, + update = util.updateProperty, + asSize = util.option.asSize, + options = this.options, + frame = this.frame; - if (!frame) { - frame = document.createElement('div'); - frame.className = 'vis timeline rootpanel'; + if (!frame) { + frame = document.createElement('div'); - var className = options.className; - if (className) { - util.addClassName(frame, util.option.asString(className)); - } - - this.frame = frame; + this.frame = frame; - changed += 1; - } - if (!frame.parentNode) { - if (!this.container) { - throw new Error('Cannot repaint root panel: no container attached'); - } - this.container.appendChild(frame); - changed += 1; + changed += 1; + } + if (!frame.parentNode) { + if (!this.container) { + throw new Error('Cannot repaint root panel: no container attached'); } + this.container.appendChild(frame); + changed += 1; + } + + frame.className = 'vis timeline rootpanel ' + options.orientation; + var className = options.className; + if (className) { + util.addClassName(frame, util.option.asString(className)); + } - changed += update(frame.style, 'top', asSize(options.top, '0px')); - changed += update(frame.style, 'left', asSize(options.left, '0px')); - changed += update(frame.style, 'width', asSize(options.width, '100%')); - changed += update(frame.style, 'height', asSize(options.height, '100%')); + changed += update(frame.style, 'top', asSize(options.top, '0px')); + changed += update(frame.style, 'left', asSize(options.left, '0px')); + changed += update(frame.style, 'width', asSize(options.width, '100%')); + changed += update(frame.style, 'height', asSize(options.height, '100%')); - this._updateEventEmitters(); - this._updateWatch(); + this._updateEventEmitters(); + this._updateWatch(); - return (changed > 0); + return (changed > 0); }; /** @@ -7831,21 +7285,21 @@ RootPanel.prototype.repaint = function () { * @return {Boolean} resized */ RootPanel.prototype.reflow = function () { - var changed = 0, - update = util.updateProperty, - frame = this.frame; - - if (frame) { - changed += update(this, 'top', frame.offsetTop); - changed += update(this, 'left', frame.offsetLeft); - changed += update(this, 'width', frame.offsetWidth); - changed += update(this, 'height', frame.offsetHeight); - } - else { - changed += 1; - } + var changed = 0, + update = util.updateProperty, + frame = this.frame; + + if (frame) { + changed += update(this, 'top', frame.offsetTop); + changed += update(this, 'left', frame.offsetLeft); + changed += update(this, 'width', frame.offsetWidth); + changed += update(this, 'height', frame.offsetHeight); + } + else { + changed += 1; + } - return (changed > 0); + return (changed > 0); }; /** @@ -7853,13 +7307,13 @@ RootPanel.prototype.reflow = function () { * @private */ RootPanel.prototype._updateWatch = function () { - var autoResize = this.getOption('autoResize'); - if (autoResize) { - this._watch(); - } - else { - this._unwatch(); - } + var autoResize = this.getOption('autoResize'); + if (autoResize) { + this._watch(); + } + else { + this._unwatch(); + } }; /** @@ -7868,31 +7322,31 @@ RootPanel.prototype._updateWatch = function () { * @private */ RootPanel.prototype._watch = function () { - var me = this; + var me = this; - this._unwatch(); + this._unwatch(); - var checkSize = function () { - var autoResize = me.getOption('autoResize'); - if (!autoResize) { - // stop watching when the option autoResize is changed to false - me._unwatch(); - return; - } + var checkSize = function () { + var autoResize = me.getOption('autoResize'); + if (!autoResize) { + // stop watching when the option autoResize is changed to false + me._unwatch(); + return; + } - if (me.frame) { - // check whether the frame is resized - if ((me.frame.clientWidth != me.width) || - (me.frame.clientHeight != me.height)) { - me.requestReflow(); - } - } - }; + if (me.frame) { + // check whether the frame is resized + if ((me.frame.clientWidth != me.width) || + (me.frame.clientHeight != me.height)) { + me.requestReflow(); + } + } + }; - // TODO: automatically cleanup the event listener when the frame is deleted - util.addEventListener(window, 'resize', checkSize); + // TODO: automatically cleanup the event listener when the frame is deleted + util.addEventListener(window, 'resize', checkSize); - this.watchTimer = setInterval(checkSize, 1000); + this.watchTimer = setInterval(checkSize, 1000); }; /** @@ -7900,12 +7354,12 @@ RootPanel.prototype._watch = function () { * @private */ RootPanel.prototype._unwatch = function () { - if (this.watchTimer) { - clearInterval(this.watchTimer); - this.watchTimer = undefined; - } + if (this.watchTimer) { + clearInterval(this.watchTimer); + this.watchTimer = undefined; + } - // TODO: remove event listener on window.resize + // TODO: remove event listener on window.resize }; /** @@ -7915,15 +7369,15 @@ RootPanel.prototype._unwatch = function () { * as parameter. */ RootPanel.prototype.on = function (event, callback) { - // register the listener at this component - var arr = this.listeners[event]; - if (!arr) { - arr = []; - this.listeners[event] = arr; - } - arr.push(callback); + // register the listener at this component + var arr = this.listeners[event]; + if (!arr) { + arr = []; + this.listeners[event] = arr; + } + arr.push(callback); - this._updateEventEmitters(); + this._updateEventEmitters(); }; /** @@ -7931,32 +7385,38 @@ RootPanel.prototype.on = function (event, callback) { * @private */ RootPanel.prototype._updateEventEmitters = function () { - if (this.listeners) { - var me = this; - util.forEach(this.listeners, function (listeners, event) { - if (!me.emitters) { - me.emitters = {}; - } - if (!(event in me.emitters)) { - // create event - var frame = me.frame; - if (frame) { - //console.log('Created a listener for event ' + event + ' on component ' + me.id); // TODO: cleanup logging - var callback = function(event) { - listeners.forEach(function (listener) { - // TODO: filter on event target! - listener(event); - }); - }; - me.emitters[event] = callback; - util.addEventListener(frame, event, callback); - } - } - }); + if (this.listeners) { + var me = this; + util.forEach(this.listeners, function (listeners, event) { + if (!me.emitters) { + me.emitters = {}; + } + if (!(event in me.emitters)) { + // create event + var frame = me.frame; + if (frame) { + //console.log('Created a listener for event ' + event + ' on component ' + me.id); // TODO: cleanup logging + var callback = function(event) { + listeners.forEach(function (listener) { + // TODO: filter on event target! + listener(event); + }); + }; + me.emitters[event] = callback; - // TODO: be able to delete event listeners - // TODO: be able to move event listeners to a parent when available - } + if (!me.hammer) { + me.hammer = Hammer(frame, { + prevent_default: true + }); + } + me.hammer.on(event, callback); + } + } + }); + + // TODO: be able to delete event listeners + // TODO: be able to move event listeners to a parent when available + } }; /** @@ -7970,41 +7430,41 @@ RootPanel.prototype._updateEventEmitters = function () { * @extends Component */ function TimeAxis (parent, depends, options) { - this.id = util.randomUUID(); - this.parent = parent; - this.depends = depends; - - this.dom = { - majorLines: [], - majorTexts: [], - minorLines: [], - minorTexts: [], - redundant: { - majorLines: [], - majorTexts: [], - minorLines: [], - minorTexts: [] - } - }; - this.props = { - range: { - start: 0, - end: 0, - minimumStep: 0 - }, - lineTop: 0 - }; - - this.options = options || {}; - this.defaultOptions = { - orientation: 'bottom', // supported: 'top', 'bottom' - // TODO: implement timeaxis orientations 'left' and 'right' - showMinorLabels: true, - showMajorLabels: true - }; - - this.conversion = null; - this.range = null; + this.id = util.randomUUID(); + this.parent = parent; + this.depends = depends; + + this.dom = { + majorLines: [], + majorTexts: [], + minorLines: [], + minorTexts: [], + redundant: { + majorLines: [], + majorTexts: [], + minorLines: [], + minorTexts: [] + } + }; + this.props = { + range: { + start: 0, + end: 0, + minimumStep: 0 + }, + lineTop: 0 + }; + + this.options = options || {}; + this.defaultOptions = { + orientation: 'bottom', // supported: 'top', 'bottom' + // TODO: implement timeaxis orientations 'left' and 'right' + showMinorLabels: true, + showMajorLabels: true + }; + + this.conversion = null; + this.range = null; } TimeAxis.prototype = new Component(); @@ -8017,11 +7477,11 @@ TimeAxis.prototype.setOptions = Component.prototype.setOptions; * @param {Range | Object} range A Range or an object containing start and end. */ TimeAxis.prototype.setRange = function (range) { - if (!(range instanceof Range) && (!range || !range.start || !range.end)) { - throw new TypeError('Range must be an instance of Range, ' + - 'or an object containing start and end.'); - } - this.range = range; + if (!(range instanceof Range) && (!range || !range.start || !range.end)) { + throw new TypeError('Range must be an instance of Range, ' + + 'or an object containing start and end.'); + } + this.range = range; }; /** @@ -8030,8 +7490,8 @@ TimeAxis.prototype.setRange = function (range) { * @return {Date} time The datetime the corresponds with given position x */ TimeAxis.prototype.toTime = function(x) { - var conversion = this.conversion; - return new Date(x / conversion.factor + conversion.offset); + var conversion = this.conversion; + return new Date(x / conversion.scale + conversion.offset); }; /** @@ -8042,8 +7502,8 @@ TimeAxis.prototype.toTime = function(x) { * @private */ TimeAxis.prototype.toScreen = function(time) { - var conversion = this.conversion; - return (time.valueOf() - conversion.offset) * conversion.factor; + var conversion = this.conversion; + return (time.valueOf() - conversion.offset) * conversion.scale; }; /** @@ -8051,112 +7511,112 @@ TimeAxis.prototype.toScreen = function(time) { * @return {Boolean} changed */ TimeAxis.prototype.repaint = function () { - var changed = 0, - update = util.updateProperty, - asSize = util.option.asSize, - options = this.options, - orientation = this.getOption('orientation'), - props = this.props, - step = this.step; - - var frame = this.frame; - if (!frame) { - frame = document.createElement('div'); - this.frame = frame; - changed += 1; - } - frame.className = 'axis ' + orientation; - // TODO: custom className? - - if (!frame.parentNode) { - if (!this.parent) { - throw new Error('Cannot repaint time axis: no parent attached'); - } - var parentContainer = this.parent.getContainer(); - if (!parentContainer) { - throw new Error('Cannot repaint time axis: parent has no container element'); - } - parentContainer.appendChild(frame); - - changed += 1; - } - - var parent = frame.parentNode; - if (parent) { - var beforeChild = frame.nextSibling; - parent.removeChild(frame); // take frame offline while updating (is almost twice as fast) - - var defaultTop = (orientation == 'bottom' && this.props.parentHeight && this.height) ? - (this.props.parentHeight - this.height) + 'px' : - '0px'; - changed += update(frame.style, 'top', asSize(options.top, defaultTop)); - changed += update(frame.style, 'left', asSize(options.left, '0px')); - changed += update(frame.style, 'width', asSize(options.width, '100%')); - changed += update(frame.style, 'height', asSize(options.height, this.height + 'px')); - - // get characters width and height - this._repaintMeasureChars(); - - if (this.step) { - this._repaintStart(); - - step.first(); - var xFirstMajorLabel = undefined; - var max = 0; - while (step.hasNext() && max < 1000) { - max++; - var cur = step.getCurrent(), - x = this.toScreen(cur), - isMajor = step.isMajor(); - - // TODO: lines must have a width, such that we can create css backgrounds - - if (this.getOption('showMinorLabels')) { - this._repaintMinorText(x, step.getLabelMinor()); - } + var changed = 0, + update = util.updateProperty, + asSize = util.option.asSize, + options = this.options, + orientation = this.getOption('orientation'), + props = this.props, + step = this.step; + + var frame = this.frame; + if (!frame) { + frame = document.createElement('div'); + this.frame = frame; + changed += 1; + } + frame.className = 'axis'; + // TODO: custom className? - if (isMajor && this.getOption('showMajorLabels')) { - if (x > 0) { - if (xFirstMajorLabel == undefined) { - xFirstMajorLabel = x; - } - this._repaintMajorText(x, step.getLabelMajor()); - } - this._repaintMajorLine(x); - } - else { - this._repaintMinorLine(x); - } + if (!frame.parentNode) { + if (!this.parent) { + throw new Error('Cannot repaint time axis: no parent attached'); + } + var parentContainer = this.parent.getContainer(); + if (!parentContainer) { + throw new Error('Cannot repaint time axis: parent has no container element'); + } + parentContainer.appendChild(frame); - step.next(); - } + changed += 1; + } + + var parent = frame.parentNode; + if (parent) { + var beforeChild = frame.nextSibling; + parent.removeChild(frame); // take frame offline while updating (is almost twice as fast) + + var defaultTop = (orientation == 'bottom' && this.props.parentHeight && this.height) ? + (this.props.parentHeight - this.height) + 'px' : + '0px'; + changed += update(frame.style, 'top', asSize(options.top, defaultTop)); + changed += update(frame.style, 'left', asSize(options.left, '0px')); + changed += update(frame.style, 'width', asSize(options.width, '100%')); + changed += update(frame.style, 'height', asSize(options.height, this.height + 'px')); - // create a major label on the left when needed - if (this.getOption('showMajorLabels')) { - var leftTime = this.toTime(0), - leftText = step.getLabelMajor(leftTime), - widthText = leftText.length * (props.majorCharWidth || 10) + 10; // upper bound estimation + // get characters width and height + this._repaintMeasureChars(); - if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) { - this._repaintMajorText(0, leftText); - } - } + if (this.step) { + this._repaintStart(); - this._repaintEnd(); - } + step.first(); + var xFirstMajorLabel = undefined; + var max = 0; + while (step.hasNext() && max < 1000) { + max++; + var cur = step.getCurrent(), + x = this.toScreen(cur), + isMajor = step.isMajor(); - this._repaintLine(); + // TODO: lines must have a width, such that we can create css backgrounds - // put frame online again - if (beforeChild) { - parent.insertBefore(frame, beforeChild); + if (this.getOption('showMinorLabels')) { + this._repaintMinorText(x, step.getLabelMinor()); + } + + if (isMajor && this.getOption('showMajorLabels')) { + if (x > 0) { + if (xFirstMajorLabel == undefined) { + xFirstMajorLabel = x; + } + this._repaintMajorText(x, step.getLabelMajor()); + } + this._repaintMajorLine(x); } else { - parent.appendChild(frame) + this._repaintMinorLine(x); + } + + step.next(); + } + + // create a major label on the left when needed + if (this.getOption('showMajorLabels')) { + var leftTime = this.toTime(0), + leftText = step.getLabelMajor(leftTime), + widthText = leftText.length * (props.majorCharWidth || 10) + 10; // upper bound estimation + + if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) { + this._repaintMajorText(0, leftText); } + } + + this._repaintEnd(); + } + + this._repaintLine(); + + // put frame online again + if (beforeChild) { + parent.insertBefore(frame, beforeChild); + } + else { + parent.appendChild(frame) } + } - return (changed > 0); + return (changed > 0); }; /** @@ -8165,18 +7625,18 @@ TimeAxis.prototype.repaint = function () { * @private */ TimeAxis.prototype._repaintStart = function () { - var dom = this.dom, - redundant = dom.redundant; + var dom = this.dom, + redundant = dom.redundant; - redundant.majorLines = dom.majorLines; - redundant.majorTexts = dom.majorTexts; - redundant.minorLines = dom.minorLines; - redundant.minorTexts = dom.minorTexts; + redundant.majorLines = dom.majorLines; + redundant.majorTexts = dom.majorTexts; + redundant.minorLines = dom.minorLines; + redundant.minorTexts = dom.minorTexts; - dom.majorLines = []; - dom.majorTexts = []; - dom.minorLines = []; - dom.minorTexts = []; + dom.majorLines = []; + dom.majorTexts = []; + dom.minorLines = []; + dom.minorTexts = []; }; /** @@ -8184,14 +7644,14 @@ TimeAxis.prototype._repaintStart = function () { * @private */ TimeAxis.prototype._repaintEnd = function () { - util.forEach(this.dom.redundant, function (arr) { - while (arr.length) { - var elem = arr.pop(); - if (elem && elem.parentNode) { - elem.parentNode.removeChild(elem); - } - } - }); + util.forEach(this.dom.redundant, function (arr) { + while (arr.length) { + var elem = arr.pop(); + if (elem && elem.parentNode) { + elem.parentNode.removeChild(elem); + } + } + }); }; @@ -8202,23 +7662,23 @@ TimeAxis.prototype._repaintEnd = function () { * @private */ TimeAxis.prototype._repaintMinorText = function (x, text) { - // reuse redundant label - var label = this.dom.redundant.minorTexts.shift(); - - if (!label) { - // create new label - var content = document.createTextNode(''); - label = document.createElement('div'); - label.appendChild(content); - label.className = 'text minor'; - this.frame.appendChild(label); - } - this.dom.minorTexts.push(label); + // reuse redundant label + var label = this.dom.redundant.minorTexts.shift(); + + if (!label) { + // create new label + var content = document.createTextNode(''); + label = document.createElement('div'); + label.appendChild(content); + label.className = 'text minor'; + this.frame.appendChild(label); + } + this.dom.minorTexts.push(label); - label.childNodes[0].nodeValue = text; - label.style.left = x + 'px'; - label.style.top = this.props.minorLabelTop + 'px'; - //label.title = title; // TODO: this is a heavy operation + label.childNodes[0].nodeValue = text; + label.style.left = x + 'px'; + label.style.top = this.props.minorLabelTop + 'px'; + //label.title = title; // TODO: this is a heavy operation }; /** @@ -8228,23 +7688,23 @@ TimeAxis.prototype._repaintMinorText = function (x, text) { * @private */ TimeAxis.prototype._repaintMajorText = function (x, text) { - // reuse redundant label - var label = this.dom.redundant.majorTexts.shift(); - - if (!label) { - // create label - var content = document.createTextNode(text); - label = document.createElement('div'); - label.className = 'text major'; - label.appendChild(content); - this.frame.appendChild(label); - } - this.dom.majorTexts.push(label); + // reuse redundant label + var label = this.dom.redundant.majorTexts.shift(); + + if (!label) { + // create label + var content = document.createTextNode(text); + label = document.createElement('div'); + label.className = 'text major'; + label.appendChild(content); + this.frame.appendChild(label); + } + this.dom.majorTexts.push(label); - label.childNodes[0].nodeValue = text; - label.style.top = this.props.majorLabelTop + 'px'; - label.style.left = x + 'px'; - //label.title = title; // TODO: this is a heavy operation + label.childNodes[0].nodeValue = text; + label.style.top = this.props.majorLabelTop + 'px'; + label.style.left = x + 'px'; + //label.title = title; // TODO: this is a heavy operation }; /** @@ -8253,21 +7713,21 @@ TimeAxis.prototype._repaintMajorText = function (x, text) { * @private */ TimeAxis.prototype._repaintMinorLine = function (x) { - // reuse redundant line - var line = this.dom.redundant.minorLines.shift(); - - if (!line) { - // create vertical line - line = document.createElement('div'); - line.className = 'grid vertical minor'; - this.frame.appendChild(line); - } - this.dom.minorLines.push(line); + // reuse redundant line + var line = this.dom.redundant.minorLines.shift(); + + if (!line) { + // create vertical line + line = document.createElement('div'); + line.className = 'grid vertical minor'; + this.frame.appendChild(line); + } + this.dom.minorLines.push(line); - var props = this.props; - line.style.top = props.minorLineTop + 'px'; - line.style.height = props.minorLineHeight + 'px'; - line.style.left = (x - props.minorLineWidth / 2) + 'px'; + var props = this.props; + line.style.top = props.minorLineTop + 'px'; + line.style.height = props.minorLineHeight + 'px'; + line.style.left = (x - props.minorLineWidth / 2) + 'px'; }; /** @@ -8276,21 +7736,21 @@ TimeAxis.prototype._repaintMinorLine = function (x) { * @private */ TimeAxis.prototype._repaintMajorLine = function (x) { - // reuse redundant line - var line = this.dom.redundant.majorLines.shift(); - - if (!line) { - // create vertical line - line = document.createElement('DIV'); - line.className = 'grid vertical major'; - this.frame.appendChild(line); - } - this.dom.majorLines.push(line); + // reuse redundant line + var line = this.dom.redundant.majorLines.shift(); + + if (!line) { + // create vertical line + line = document.createElement('DIV'); + line.className = 'grid vertical major'; + this.frame.appendChild(line); + } + this.dom.majorLines.push(line); - var props = this.props; - line.style.top = props.majorLineTop + 'px'; - line.style.left = (x - props.majorLineWidth / 2) + 'px'; - line.style.height = props.majorLineHeight + 'px'; + var props = this.props; + line.style.top = props.majorLineTop + 'px'; + line.style.left = (x - props.majorLineWidth / 2) + 'px'; + line.style.height = props.majorLineHeight + 'px'; }; @@ -8299,33 +7759,33 @@ TimeAxis.prototype._repaintMajorLine = function (x) { * @private */ TimeAxis.prototype._repaintLine = function() { - var line = this.dom.line, - frame = this.frame, - options = this.options; - - // line before all axis elements - if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) { - if (line) { - // put this line at the end of all childs - frame.removeChild(line); - frame.appendChild(line); - } - else { - // create the axis line - line = document.createElement('div'); - line.className = 'grid horizontal major'; - frame.appendChild(line); - this.dom.line = line; - } + var line = this.dom.line, + frame = this.frame, + options = this.options; - line.style.top = this.props.lineTop + 'px'; + // line before all axis elements + if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) { + if (line) { + // put this line at the end of all childs + frame.removeChild(line); + frame.appendChild(line); } else { - if (line && axis.parentElement) { - frame.removeChild(axis.line); - delete this.dom.line; - } + // create the axis line + line = document.createElement('div'); + line.className = 'grid horizontal major'; + frame.appendChild(line); + this.dom.line = line; + } + + line.style.top = this.props.lineTop + 'px'; + } + else { + if (line && line.parentElement) { + frame.removeChild(line.line); + delete this.dom.line; } + } }; /** @@ -8333,31 +7793,31 @@ TimeAxis.prototype._repaintLine = function() { * @private */ TimeAxis.prototype._repaintMeasureChars = function () { - // calculate the width and height of a single character - // this is used to calculate the step size, and also the positioning of the - // axis - var dom = this.dom, - text; - - if (!dom.measureCharMinor) { - text = document.createTextNode('0'); - var measureCharMinor = document.createElement('DIV'); - measureCharMinor.className = 'text minor measure'; - measureCharMinor.appendChild(text); - this.frame.appendChild(measureCharMinor); - - dom.measureCharMinor = measureCharMinor; - } + // calculate the width and height of a single character + // this is used to calculate the step size, and also the positioning of the + // axis + var dom = this.dom, + text; + + if (!dom.measureCharMinor) { + text = document.createTextNode('0'); + var measureCharMinor = document.createElement('DIV'); + measureCharMinor.className = 'text minor measure'; + measureCharMinor.appendChild(text); + this.frame.appendChild(measureCharMinor); + + dom.measureCharMinor = measureCharMinor; + } - if (!dom.measureCharMajor) { - text = document.createTextNode('0'); - var measureCharMajor = document.createElement('DIV'); - measureCharMajor.className = 'text major measure'; - measureCharMajor.appendChild(text); - this.frame.appendChild(measureCharMajor); + if (!dom.measureCharMajor) { + text = document.createTextNode('0'); + var measureCharMajor = document.createElement('DIV'); + measureCharMajor.className = 'text major measure'; + measureCharMajor.appendChild(text); + this.frame.appendChild(measureCharMajor); - dom.measureCharMajor = measureCharMajor; - } + dom.measureCharMajor = measureCharMajor; + } }; /** @@ -8365,121 +7825,121 @@ TimeAxis.prototype._repaintMeasureChars = function () { * @return {Boolean} resized */ TimeAxis.prototype.reflow = function () { - var changed = 0, - update = util.updateProperty, - frame = this.frame, - range = this.range; + var changed = 0, + update = util.updateProperty, + frame = this.frame, + range = this.range; - if (!range) { - throw new Error('Cannot repaint time axis: no range configured'); - } + if (!range) { + throw new Error('Cannot repaint time axis: no range configured'); + } - if (frame) { - changed += update(this, 'top', frame.offsetTop); - changed += update(this, 'left', frame.offsetLeft); + if (frame) { + changed += update(this, 'top', frame.offsetTop); + changed += update(this, 'left', frame.offsetLeft); - // calculate size of a character - var props = this.props, - showMinorLabels = this.getOption('showMinorLabels'), - showMajorLabels = this.getOption('showMajorLabels'), - measureCharMinor = this.dom.measureCharMinor, - measureCharMajor = this.dom.measureCharMajor; - if (measureCharMinor) { - props.minorCharHeight = measureCharMinor.clientHeight; - props.minorCharWidth = measureCharMinor.clientWidth; - } - if (measureCharMajor) { - props.majorCharHeight = measureCharMajor.clientHeight; - props.majorCharWidth = measureCharMajor.clientWidth; - } + // calculate size of a character + var props = this.props, + showMinorLabels = this.getOption('showMinorLabels'), + showMajorLabels = this.getOption('showMajorLabels'), + measureCharMinor = this.dom.measureCharMinor, + measureCharMajor = this.dom.measureCharMajor; + if (measureCharMinor) { + props.minorCharHeight = measureCharMinor.clientHeight; + props.minorCharWidth = measureCharMinor.clientWidth; + } + if (measureCharMajor) { + props.majorCharHeight = measureCharMajor.clientHeight; + props.majorCharWidth = measureCharMajor.clientWidth; + } - var parentHeight = frame.parentNode ? frame.parentNode.offsetHeight : 0; - if (parentHeight != props.parentHeight) { - props.parentHeight = parentHeight; - changed += 1; - } - switch (this.getOption('orientation')) { - case 'bottom': - props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; - props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0; + var parentHeight = frame.parentNode ? frame.parentNode.offsetHeight : 0; + if (parentHeight != props.parentHeight) { + props.parentHeight = parentHeight; + changed += 1; + } + switch (this.getOption('orientation')) { + case 'bottom': + props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; + props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0; - props.minorLabelTop = 0; - props.majorLabelTop = props.minorLabelTop + props.minorLabelHeight; + props.minorLabelTop = 0; + props.majorLabelTop = props.minorLabelTop + props.minorLabelHeight; - props.minorLineTop = -this.top; - props.minorLineHeight = Math.max(this.top + props.majorLabelHeight, 0); - props.minorLineWidth = 1; // TODO: really calculate width + props.minorLineTop = -this.top; + props.minorLineHeight = Math.max(this.top + props.majorLabelHeight, 0); + props.minorLineWidth = 1; // TODO: really calculate width - props.majorLineTop = -this.top; - props.majorLineHeight = Math.max(this.top + props.minorLabelHeight + props.majorLabelHeight, 0); - props.majorLineWidth = 1; // TODO: really calculate width + props.majorLineTop = -this.top; + props.majorLineHeight = Math.max(this.top + props.minorLabelHeight + props.majorLabelHeight, 0); + props.majorLineWidth = 1; // TODO: really calculate width - props.lineTop = 0; + props.lineTop = 0; - break; + break; - case 'top': - props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; - props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0; + case 'top': + props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; + props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0; - props.majorLabelTop = 0; - props.minorLabelTop = props.majorLabelTop + props.majorLabelHeight; + props.majorLabelTop = 0; + props.minorLabelTop = props.majorLabelTop + props.majorLabelHeight; - props.minorLineTop = props.minorLabelTop; - props.minorLineHeight = Math.max(parentHeight - props.majorLabelHeight - this.top); - props.minorLineWidth = 1; // TODO: really calculate width + props.minorLineTop = props.minorLabelTop; + props.minorLineHeight = Math.max(parentHeight - props.majorLabelHeight - this.top); + props.minorLineWidth = 1; // TODO: really calculate width - props.majorLineTop = 0; - props.majorLineHeight = Math.max(parentHeight - this.top); - props.majorLineWidth = 1; // TODO: really calculate width + props.majorLineTop = 0; + props.majorLineHeight = Math.max(parentHeight - this.top); + props.majorLineWidth = 1; // TODO: really calculate width - props.lineTop = props.majorLabelHeight + props.minorLabelHeight; + props.lineTop = props.majorLabelHeight + props.minorLabelHeight; - break; + break; - default: - throw new Error('Unkown orientation "' + this.getOption('orientation') + '"'); - } + default: + throw new Error('Unkown orientation "' + this.getOption('orientation') + '"'); + } - var height = props.minorLabelHeight + props.majorLabelHeight; - changed += update(this, 'width', frame.offsetWidth); - changed += update(this, 'height', height); + var height = props.minorLabelHeight + props.majorLabelHeight; + changed += update(this, 'width', frame.offsetWidth); + changed += update(this, 'height', height); - // calculate range and step - this._updateConversion(); + // calculate range and step + this._updateConversion(); - var start = util.convert(range.start, 'Number'), - end = util.convert(range.end, 'Number'), - minimumStep = this.toTime((props.minorCharWidth || 10) * 5).valueOf() - -this.toTime(0).valueOf(); - this.step = new TimeStep(new Date(start), new Date(end), minimumStep); - changed += update(props.range, 'start', start); - changed += update(props.range, 'end', end); - changed += update(props.range, 'minimumStep', minimumStep.valueOf()); - } + var start = util.convert(range.start, 'Number'), + end = util.convert(range.end, 'Number'), + minimumStep = this.toTime((props.minorCharWidth || 10) * 5).valueOf() + -this.toTime(0).valueOf(); + this.step = new TimeStep(new Date(start), new Date(end), minimumStep); + changed += update(props.range, 'start', start); + changed += update(props.range, 'end', end); + changed += update(props.range, 'minimumStep', minimumStep.valueOf()); + } - return (changed > 0); + return (changed > 0); }; /** - * Calculate the factor and offset to convert a position on screen to the + * Calculate the scale and offset to convert a position on screen to the * corresponding date and vice versa. * After the method _updateConversion is executed once, the methods toTime * and toScreen can be used. * @private */ TimeAxis.prototype._updateConversion = function() { - var range = this.range; - if (!range) { - throw new Error('No range configured'); - } + var range = this.range; + if (!range) { + throw new Error('No range configured'); + } - if (range.conversion) { - this.conversion = range.conversion(this.width); - } - else { - this.conversion = Range.conversion(range.start, range.end, this.width); - } + if (range.conversion) { + this.conversion = range.conversion(this.width); + } + else { + this.conversion = Range.conversion(range.start, range.end, this.width); + } }; /** @@ -8494,14 +7954,14 @@ TimeAxis.prototype._updateConversion = function() { */ function CurrentTime (parent, depends, options) { - this.id = util.randomUUID(); - this.parent = parent; - this.depends = depends; - - this.options = options || {}; - this.defaultOptions = { - showCurrentTime: false - }; + this.id = util.randomUUID(); + this.parent = parent; + this.depends = depends; + + this.options = options || {}; + this.defaultOptions = { + showCurrentTime: false + }; } CurrentTime.prototype = new Component(); @@ -8514,7 +7974,7 @@ CurrentTime.prototype.setOptions = Component.prototype.setOptions; * @returns {HTMLElement} container */ CurrentTime.prototype.getContainer = function () { - return this.frame; + return this.frame; }; /** @@ -8522,66 +7982,66 @@ CurrentTime.prototype.getContainer = function () { * @return {Boolean} changed */ CurrentTime.prototype.repaint = function () { - var bar = this.frame, - parent = this.parent, - parentContainer = parent.parent.getContainer(); + var bar = this.frame, + parent = this.parent, + parentContainer = parent.parent.getContainer(); - if (!parent) { - throw new Error('Cannot repaint bar: no parent attached'); - } + if (!parent) { + throw new Error('Cannot repaint bar: no parent attached'); + } - if (!parentContainer) { - throw new Error('Cannot repaint bar: parent has no container element'); + if (!parentContainer) { + throw new Error('Cannot repaint bar: parent has no container element'); + } + + if (!this.getOption('showCurrentTime')) { + if (bar) { + parentContainer.removeChild(bar); + delete this.frame; } - if (!this.getOption('showCurrentTime')) { - if (bar) { - parentContainer.removeChild(bar); - delete this.frame; - } + return; + } - return; - } + if (!bar) { + bar = document.createElement('div'); + bar.className = 'currenttime'; + bar.style.position = 'absolute'; + bar.style.top = '0px'; + bar.style.height = '100%'; - if (!bar) { - bar = document.createElement('div'); - bar.className = 'currenttime'; - bar.style.position = 'absolute'; - bar.style.top = '0px'; - bar.style.height = '100%'; + parentContainer.appendChild(bar); + this.frame = bar; + } - parentContainer.appendChild(bar); - this.frame = bar; - } + if (!parent.conversion) { + parent._updateConversion(); + } - if (!parent.conversion) { - parent._updateConversion(); - } + var now = new Date(); + var x = parent.toScreen(now); - var now = new Date(); - var x = parent.toScreen(now); + bar.style.left = x + 'px'; + bar.title = 'Current time: ' + now; - bar.style.left = x + 'px'; - bar.title = 'Current time: ' + now; + // start a timer to adjust for the new time + if (this.currentTimeTimer !== undefined) { + clearTimeout(this.currentTimeTimer); + delete this.currentTimeTimer; + } - // start a timer to adjust for the new time - if (this.currentTimeTimer !== undefined) { - clearTimeout(this.currentTimeTimer); - delete this.currentTimeTimer; - } + var timeline = this; + var interval = 1 / parent.conversion.scale / 2; - var timeline = this; - var interval = 1 / parent.conversion.factor / 2; + if (interval < 30) { + interval = 30; + } - if (interval < 30) { - interval = 30; - } - - this.currentTimeTimer = setTimeout(function() { - timeline.repaint(); - }, interval); + this.currentTimeTimer = setTimeout(function() { + timeline.repaint(); + }, interval); - return false; + return false; }; /** @@ -8596,17 +8056,17 @@ CurrentTime.prototype.repaint = function () { */ function CustomTime (parent, depends, options) { - this.id = util.randomUUID(); - this.parent = parent; - this.depends = depends; + this.id = util.randomUUID(); + this.parent = parent; + this.depends = depends; - this.options = options || {}; - this.defaultOptions = { - showCustomTime: false - }; + this.options = options || {}; + this.defaultOptions = { + showCustomTime: false + }; - this.listeners = []; - this.customTime = new Date(); + this.listeners = []; + this.customTime = new Date(); } CustomTime.prototype = new Component(); @@ -8619,7 +8079,7 @@ CustomTime.prototype.setOptions = Component.prototype.setOptions; * @returns {HTMLElement} container */ CustomTime.prototype.getContainer = function () { - return this.frame; + return this.frame; }; /** @@ -8627,59 +8087,59 @@ CustomTime.prototype.getContainer = function () { * @return {Boolean} changed */ CustomTime.prototype.repaint = function () { - var bar = this.frame, - parent = this.parent, - parentContainer = parent.parent.getContainer(); + var bar = this.frame, + parent = this.parent, + parentContainer = parent.parent.getContainer(); - if (!parent) { - throw new Error('Cannot repaint bar: no parent attached'); - } - - if (!parentContainer) { - throw new Error('Cannot repaint bar: parent has no container element'); - } + if (!parent) { + throw new Error('Cannot repaint bar: no parent attached'); + } - if (!this.getOption('showCustomTime')) { - if (bar) { - parentContainer.removeChild(bar); - delete this.frame; - } + if (!parentContainer) { + throw new Error('Cannot repaint bar: parent has no container element'); + } - return; + if (!this.getOption('showCustomTime')) { + if (bar) { + parentContainer.removeChild(bar); + delete this.frame; } - if (!bar) { - bar = document.createElement('div'); - bar.className = 'customtime'; - bar.style.position = 'absolute'; - bar.style.top = '0px'; - bar.style.height = '100%'; + return; + } - parentContainer.appendChild(bar); + if (!bar) { + bar = document.createElement('div'); + bar.className = 'customtime'; + bar.style.position = 'absolute'; + bar.style.top = '0px'; + bar.style.height = '100%'; - var drag = document.createElement('div'); - drag.style.position = 'relative'; - drag.style.top = '0px'; - drag.style.left = '-10px'; - drag.style.height = '100%'; - drag.style.width = '20px'; - bar.appendChild(drag); + parentContainer.appendChild(bar); - this.frame = bar; + var drag = document.createElement('div'); + drag.style.position = 'relative'; + drag.style.top = '0px'; + drag.style.left = '-10px'; + drag.style.height = '100%'; + drag.style.width = '20px'; + bar.appendChild(drag); - this.subscribe(this, 'movetime'); - } + this.frame = bar; - if (!parent.conversion) { - parent._updateConversion(); - } + this.subscribe(this, 'movetime'); + } - var x = parent.toScreen(this.customTime); + if (!parent.conversion) { + parent._updateConversion(); + } - bar.style.left = x + 'px'; - bar.title = 'Time: ' + this.customTime; + var x = parent.toScreen(this.customTime); - return false; + bar.style.left = x + 'px'; + bar.title = 'Time: ' + this.customTime; + + return false; }; /** @@ -8687,8 +8147,8 @@ CustomTime.prototype.repaint = function () { * @param {Date} time */ CustomTime.prototype._setCustomTime = function(time) { - this.customTime = new Date(time.valueOf()); - this.repaint(); + this.customTime = new Date(time.valueOf()); + this.repaint(); }; /** @@ -8696,7 +8156,7 @@ CustomTime.prototype._setCustomTime = function(time) { * @return {Date} customTime */ CustomTime.prototype._getCustomTime = function() { - return new Date(this.customTime.valueOf()); + return new Date(this.customTime.valueOf()); }; /** @@ -8704,18 +8164,18 @@ CustomTime.prototype._getCustomTime = function() { * @param {Component} component */ CustomTime.prototype.subscribe = function (component, event) { - var me = this; - var listener = { - component: component, - event: event, - callback: function (event) { - me._onMouseDown(event, listener); - }, - params: {} - }; + var me = this; + var listener = { + component: component, + event: event, + callback: function (event) { + me._onMouseDown(event, listener); + }, + params: {} + }; - component.on('mousedown', listener.callback); - me.listeners.push(listener); + component.on('mousedown', listener.callback); + me.listeners.push(listener); }; @@ -8726,13 +8186,13 @@ CustomTime.prototype.subscribe = function (component, event) { * as parameter. */ CustomTime.prototype.on = function (event, callback) { - var bar = this.frame; - if (!bar) { - throw new Error('Cannot add event listener: no parent attached'); - } + var bar = this.frame; + if (!bar) { + throw new Error('Cannot add event listener: no parent attached'); + } - events.addListener(this, event, callback); - util.addEventListener(bar, event, callback); + events.addListener(this, event, callback); + util.addEventListener(bar, event, callback); }; /** @@ -8742,38 +8202,38 @@ CustomTime.prototype.on = function (event, callback) { * @private */ CustomTime.prototype._onMouseDown = function(event, listener) { - event = event || window.event; - var params = listener.params; + event = event || window.event; + var params = listener.params; - // only react on left mouse button down - var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1); - if (!leftButtonDown) { - return; - } + // only react on left mouse button down + var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1); + if (!leftButtonDown) { + return; + } - // get mouse position - params.mouseX = util.getPageX(event); - params.moved = false; - - params.customTime = this.customTime; + // get mouse position + params.mouseX = util.getPageX(event); + params.moved = false; - // add event listeners to handle moving the custom time bar - var me = this; - if (!params.onMouseMove) { - params.onMouseMove = function (event) { - me._onMouseMove(event, listener); - }; - util.addEventListener(document, 'mousemove', params.onMouseMove); - } - if (!params.onMouseUp) { - params.onMouseUp = function (event) { - me._onMouseUp(event, listener); - }; - util.addEventListener(document, 'mouseup', params.onMouseUp); - } + params.customTime = this.customTime; + + // add event listeners to handle moving the custom time bar + var me = this; + if (!params.onMouseMove) { + params.onMouseMove = function (event) { + me._onMouseMove(event, listener); + }; + util.addEventListener(document, 'mousemove', params.onMouseMove); + } + if (!params.onMouseUp) { + params.onMouseUp = function (event) { + me._onMouseUp(event, listener); + }; + util.addEventListener(document, 'mouseup', params.onMouseUp); + } - util.stopPropagation(event); - util.preventDefault(event); + util.stopPropagation(event); + util.preventDefault(event); }; /** @@ -8784,33 +8244,33 @@ CustomTime.prototype._onMouseDown = function(event, listener) { * @private */ CustomTime.prototype._onMouseMove = function (event, listener) { - event = event || window.event; - var params = listener.params; - var parent = this.parent; + event = event || window.event; + var params = listener.params; + var parent = this.parent; - // calculate change in mouse position - var mouseX = util.getPageX(event); + // calculate change in mouse position + var mouseX = util.getPageX(event); - if (params.mouseX === undefined) { - params.mouseX = mouseX; - } + if (params.mouseX === undefined) { + params.mouseX = mouseX; + } - var diff = mouseX - params.mouseX; + var diff = mouseX - params.mouseX; - // if mouse movement is big enough, register it as a "moved" event - if (Math.abs(diff) >= 1) { - params.moved = true; - } + // if mouse movement is big enough, register it as a "moved" event + if (Math.abs(diff) >= 1) { + params.moved = true; + } - var x = parent.toScreen(params.customTime); - var xnew = x + diff; - var time = parent.toTime(xnew); - this._setCustomTime(time); + var x = parent.toScreen(params.customTime); + var xnew = x + diff; + var time = parent.toTime(xnew); + this._setCustomTime(time); - // fire a timechange event - events.trigger(this, 'timechange', {customTime: this.customTime}); + // fire a timechange event + events.trigger(this, 'timechange', {customTime: this.customTime}); - util.preventDefault(event); + util.preventDefault(event); }; /** @@ -8821,23 +8281,23 @@ CustomTime.prototype._onMouseMove = function (event, listener) { * @private */ CustomTime.prototype._onMouseUp = function (event, listener) { - event = event || window.event; - var params = listener.params; + event = event || window.event; + var params = listener.params; - // remove event listeners here, important for Safari - if (params.onMouseMove) { - util.removeEventListener(document, 'mousemove', params.onMouseMove); - params.onMouseMove = null; - } - if (params.onMouseUp) { - util.removeEventListener(document, 'mouseup', params.onMouseUp); - params.onMouseUp = null; - } + // remove event listeners here, important for Safari + if (params.onMouseMove) { + util.removeEventListener(document, 'mousemove', params.onMouseMove); + params.onMouseMove = null; + } + if (params.onMouseUp) { + util.removeEventListener(document, 'mouseup', params.onMouseUp); + params.onMouseUp = null; + } - if (params.moved) { - // fire a timechanged event - events.trigger(this, 'timechanged', {customTime: this.customTime}); - } + if (params.moved) { + // fire a timechanged event + events.trigger(this, 'timechanged', {customTime: this.customTime}); + } }; /** @@ -8854,63 +8314,63 @@ CustomTime.prototype._onMouseUp = function (event, listener) { */ // TODO: improve performance by replacing all Array.forEach with a for loop function ItemSet(parent, depends, options) { - this.id = util.randomUUID(); - this.parent = parent; - this.depends = depends; - - // one options object is shared by this itemset and all its items - this.options = options || {}; - this.defaultOptions = { - type: 'box', - align: 'center', - orientation: 'bottom', - margin: { - axis: 20, - item: 10 - }, - padding: 5 - }; - - this.dom = {}; - - var me = this; - this.itemsData = null; // DataSet - this.range = null; // Range or Object {start: number, end: number} - - this.listeners = { - 'add': function (event, params, senderId) { - if (senderId != me.id) { - me._onAdd(params.items); - } - }, - 'update': function (event, params, senderId) { - if (senderId != me.id) { - me._onUpdate(params.items); - } - }, - 'remove': function (event, params, senderId) { - if (senderId != me.id) { - me._onRemove(params.items); - } - } - }; + this.id = util.randomUUID(); + this.parent = parent; + this.depends = depends; + + // one options object is shared by this itemset and all its items + this.options = options || {}; + this.defaultOptions = { + type: 'box', + align: 'center', + orientation: 'bottom', + margin: { + axis: 20, + item: 10 + }, + padding: 5 + }; - this.items = {}; // object with an Item for every data item - this.queue = {}; // queue with id/actions: 'add', 'update', 'delete' - this.stack = new Stack(this, Object.create(this.options)); - this.conversion = null; + this.dom = {}; - // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis -} + var me = this; + this.itemsData = null; // DataSet + this.range = null; // Range or Object {start: number, end: number} + + this.listeners = { + 'add': function (event, params, senderId) { + if (senderId != me.id) { + me._onAdd(params.items); + } + }, + 'update': function (event, params, senderId) { + if (senderId != me.id) { + me._onUpdate(params.items); + } + }, + 'remove': function (event, params, senderId) { + if (senderId != me.id) { + me._onRemove(params.items); + } + } + }; + + this.items = {}; // object with an Item for every data item + this.queue = {}; // queue with id/actions: 'add', 'update', 'delete' + this.stack = new Stack(this, Object.create(this.options)); + this.conversion = null; + + // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis +} ItemSet.prototype = new Panel(); // available item types will be registered here ItemSet.types = { - box: ItemBox, - range: ItemRange, - rangeoverflow: ItemRangeOverflow, - point: ItemPoint + box: ItemBox, + range: ItemRange, + rangeoverflow: ItemRangeOverflow, + point: ItemPoint }; /** @@ -8945,11 +8405,11 @@ ItemSet.prototype.setOptions = Component.prototype.setOptions; * @param {Range | Object} range A Range or an object containing start and end. */ ItemSet.prototype.setRange = function setRange(range) { - if (!(range instanceof Range) && (!range || !range.start || !range.end)) { - throw new TypeError('Range must be an instance of Range, ' + - 'or an object containing start and end.'); - } - this.range = range; + if (!(range instanceof Range) && (!range || !range.start || !range.end)) { + throw new TypeError('Range must be an instance of Range, ' + + 'or an object containing start and end.'); + } + this.range = range; }; /** @@ -8957,170 +8417,170 @@ ItemSet.prototype.setRange = function setRange(range) { * @return {Boolean} changed */ ItemSet.prototype.repaint = function repaint() { - var changed = 0, - update = util.updateProperty, - asSize = util.option.asSize, - options = this.options, - orientation = this.getOption('orientation'), - defaultOptions = this.defaultOptions, - frame = this.frame; - - if (!frame) { - frame = document.createElement('div'); - frame.className = 'itemset'; - - var className = options.className; - if (className) { - util.addClassName(frame, util.option.asString(className)); - } - - // create background panel - var background = document.createElement('div'); - background.className = 'background'; - frame.appendChild(background); - this.dom.background = background; - - // create foreground panel - var foreground = document.createElement('div'); - foreground.className = 'foreground'; - frame.appendChild(foreground); - this.dom.foreground = foreground; - - // create axis panel - var axis = document.createElement('div'); - axis.className = 'itemset-axis'; - //frame.appendChild(axis); - this.dom.axis = axis; - - this.frame = frame; - changed += 1; - } - - if (!this.parent) { - throw new Error('Cannot repaint itemset: no parent attached'); - } - var parentContainer = this.parent.getContainer(); - if (!parentContainer) { - throw new Error('Cannot repaint itemset: parent has no container element'); - } - if (!frame.parentNode) { - parentContainer.appendChild(frame); - changed += 1; - } - if (!this.dom.axis.parentNode) { - parentContainer.appendChild(this.dom.axis); - changed += 1; + var changed = 0, + update = util.updateProperty, + asSize = util.option.asSize, + options = this.options, + orientation = this.getOption('orientation'), + defaultOptions = this.defaultOptions, + frame = this.frame; + + if (!frame) { + frame = document.createElement('div'); + frame.className = 'itemset'; + + var className = options.className; + if (className) { + util.addClassName(frame, util.option.asString(className)); } - // reposition frame - changed += update(frame.style, 'left', asSize(options.left, '0px')); - changed += update(frame.style, 'top', asSize(options.top, '0px')); - changed += update(frame.style, 'width', asSize(options.width, '100%')); - changed += update(frame.style, 'height', asSize(options.height, this.height + 'px')); + // create background panel + var background = document.createElement('div'); + background.className = 'background'; + frame.appendChild(background); + this.dom.background = background; - // reposition axis - changed += update(this.dom.axis.style, 'left', asSize(options.left, '0px')); - changed += update(this.dom.axis.style, 'width', asSize(options.width, '100%')); - if (orientation == 'bottom') { - changed += update(this.dom.axis.style, 'top', (this.height + this.top) + 'px'); - } - else { // orientation == 'top' - changed += update(this.dom.axis.style, 'top', this.top + 'px'); - } + // create foreground panel + var foreground = document.createElement('div'); + foreground.className = 'foreground'; + frame.appendChild(foreground); + this.dom.foreground = foreground; - this._updateConversion(); + // create axis panel + var axis = document.createElement('div'); + axis.className = 'itemset-axis'; + //frame.appendChild(axis); + this.dom.axis = axis; - var me = this, - queue = this.queue, - itemsData = this.itemsData, - items = this.items, - dataOptions = { - // TODO: cleanup - // fields: [(itemsData && itemsData.fieldId || 'id'), 'start', 'end', 'content', 'type', 'className'] - }; + this.frame = frame; + changed += 1; + } - // show/hide added/changed/removed items - Object.keys(queue).forEach(function (id) { - //var entry = queue[id]; - var action = queue[id]; - var item = items[id]; - //var item = entry.item; - //noinspection FallthroughInSwitchStatementJS - switch (action) { - case 'add': - case 'update': - var itemData = itemsData && itemsData.get(id, dataOptions); - - if (itemData) { - var type = itemData.type || - (itemData.start && itemData.end && 'range') || - options.type || - 'box'; - var constructor = ItemSet.types[type]; - - // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error? - if (item) { - // update item - if (!constructor || !(item instanceof constructor)) { - // item type has changed, hide and delete the item - changed += item.hide(); - item = null; - } - else { - item.data = itemData; // TODO: create a method item.setData ? - changed++; - } - } + if (!this.parent) { + throw new Error('Cannot repaint itemset: no parent attached'); + } + var parentContainer = this.parent.getContainer(); + if (!parentContainer) { + throw new Error('Cannot repaint itemset: parent has no container element'); + } + if (!frame.parentNode) { + parentContainer.appendChild(frame); + changed += 1; + } + if (!this.dom.axis.parentNode) { + parentContainer.appendChild(this.dom.axis); + changed += 1; + } - if (!item) { - // create item - if (constructor) { - item = new constructor(me, itemData, options, defaultOptions); - changed++; - } - else { - throw new TypeError('Unknown item type "' + type + '"'); - } - } + // reposition frame + changed += update(frame.style, 'left', asSize(options.left, '0px')); + changed += update(frame.style, 'top', asSize(options.top, '0px')); + changed += update(frame.style, 'width', asSize(options.width, '100%')); + changed += update(frame.style, 'height', asSize(options.height, this.height + 'px')); + + // reposition axis + changed += update(this.dom.axis.style, 'left', asSize(options.left, '0px')); + changed += update(this.dom.axis.style, 'width', asSize(options.width, '100%')); + if (orientation == 'bottom') { + changed += update(this.dom.axis.style, 'top', (this.height + this.top) + 'px'); + } + else { // orientation == 'top' + changed += update(this.dom.axis.style, 'top', this.top + 'px'); + } - // force a repaint (not only a reposition) - item.repaint(); + this._updateConversion(); + + var me = this, + queue = this.queue, + itemsData = this.itemsData, + items = this.items, + dataOptions = { + // TODO: cleanup + // fields: [(itemsData && itemsData.fieldId || 'id'), 'start', 'end', 'content', 'type', 'className'] + }; + + // show/hide added/changed/removed items + Object.keys(queue).forEach(function (id) { + //var entry = queue[id]; + var action = queue[id]; + var item = items[id]; + //var item = entry.item; + //noinspection FallthroughInSwitchStatementJS + switch (action) { + case 'add': + case 'update': + var itemData = itemsData && itemsData.get(id, dataOptions); + + if (itemData) { + var type = itemData.type || + (itemData.start && itemData.end && 'range') || + options.type || + 'box'; + var constructor = ItemSet.types[type]; + + // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error? + if (item) { + // update item + if (!constructor || !(item instanceof constructor)) { + // item type has changed, hide and delete the item + changed += item.hide(); + item = null; + } + else { + item.data = itemData; // TODO: create a method item.setData ? + changed++; + } + } - items[id] = item; - } + if (!item) { + // create item + if (constructor) { + item = new constructor(me, itemData, options, defaultOptions); + changed++; + } + else { + throw new TypeError('Unknown item type "' + type + '"'); + } + } - // update queue - delete queue[id]; - break; + // force a repaint (not only a reposition) + item.repaint(); - case 'remove': - if (item) { - // remove DOM of the item - changed += item.hide(); - } + items[id] = item; + } - // update lists - delete items[id]; - delete queue[id]; - break; + // update queue + delete queue[id]; + break; - default: - console.log('Error: unknown action "' + action + '"'); + case 'remove': + if (item) { + // remove DOM of the item + changed += item.hide(); } - }); - // reposition all items. Show items only when in the visible area - util.forEach(this.items, function (item) { - if (item.visible) { - changed += item.show(); - item.reposition(); - } - else { - changed += item.hide(); - } - }); + // update lists + delete items[id]; + delete queue[id]; + break; + + default: + console.log('Error: unknown action "' + action + '"'); + } + }); + + // reposition all items. Show items only when in the visible area + util.forEach(this.items, function (item) { + if (item.visible) { + changed += item.show(); + item.reposition(); + } + else { + changed += item.hide(); + } + }); - return (changed > 0); + return (changed > 0); }; /** @@ -9128,7 +8588,7 @@ ItemSet.prototype.repaint = function repaint() { * @return {HTMLElement} foreground */ ItemSet.prototype.getForeground = function getForeground() { - return this.dom.foreground; + return this.dom.foreground; }; /** @@ -9136,7 +8596,7 @@ ItemSet.prototype.getForeground = function getForeground() { * @return {HTMLElement} background */ ItemSet.prototype.getBackground = function getBackground() { - return this.dom.background; + return this.dom.background; }; /** @@ -9144,7 +8604,7 @@ ItemSet.prototype.getBackground = function getBackground() { * @return {HTMLElement} axis */ ItemSet.prototype.getAxis = function getAxis() { - return this.dom.axis; + return this.dom.axis; }; /** @@ -9152,63 +8612,63 @@ ItemSet.prototype.getAxis = function getAxis() { * @return {Boolean} resized */ ItemSet.prototype.reflow = function reflow () { - var changed = 0, - options = this.options, - marginAxis = options.margin && options.margin.axis || this.defaultOptions.margin.axis, - marginItem = options.margin && options.margin.item || this.defaultOptions.margin.item, - update = util.updateProperty, - asNumber = util.option.asNumber, - asSize = util.option.asSize, - frame = this.frame; - - if (frame) { - this._updateConversion(); - - util.forEach(this.items, function (item) { - changed += item.reflow(); - }); + var changed = 0, + options = this.options, + marginAxis = options.margin && options.margin.axis || this.defaultOptions.margin.axis, + marginItem = options.margin && options.margin.item || this.defaultOptions.margin.item, + update = util.updateProperty, + asNumber = util.option.asNumber, + asSize = util.option.asSize, + frame = this.frame; + + if (frame) { + this._updateConversion(); - // TODO: stack.update should be triggered via an event, in stack itself - // TODO: only update the stack when there are changed items - this.stack.update(); + util.forEach(this.items, function (item) { + changed += item.reflow(); + }); - var maxHeight = asNumber(options.maxHeight); - var fixedHeight = (asSize(options.height) != null); - var height; - if (fixedHeight) { - height = frame.offsetHeight; - } - else { - // height is not specified, determine the height from the height and positioned items - var visibleItems = this.stack.ordered; // TODO: not so nice way to get the filtered items - if (visibleItems.length) { - var min = visibleItems[0].top; - var max = visibleItems[0].top + visibleItems[0].height; - util.forEach(visibleItems, function (item) { - min = Math.min(min, item.top); - max = Math.max(max, (item.top + item.height)); - }); - height = (max - min) + marginAxis + marginItem; - } - else { - height = marginAxis + marginItem; - } - } - if (maxHeight != null) { - height = Math.min(height, maxHeight); - } - changed += update(this, 'height', height); + // TODO: stack.update should be triggered via an event, in stack itself + // TODO: only update the stack when there are changed items + this.stack.update(); - // calculate height from items - changed += update(this, 'top', frame.offsetTop); - changed += update(this, 'left', frame.offsetLeft); - changed += update(this, 'width', frame.offsetWidth); + var maxHeight = asNumber(options.maxHeight); + var fixedHeight = (asSize(options.height) != null); + var height; + if (fixedHeight) { + height = frame.offsetHeight; } else { - changed += 1; + // height is not specified, determine the height from the height and positioned items + var visibleItems = this.stack.ordered; // TODO: not so nice way to get the filtered items + if (visibleItems.length) { + var min = visibleItems[0].top; + var max = visibleItems[0].top + visibleItems[0].height; + util.forEach(visibleItems, function (item) { + min = Math.min(min, item.top); + max = Math.max(max, (item.top + item.height)); + }); + height = (max - min) + marginAxis + marginItem; + } + else { + height = marginAxis + marginItem; + } + } + if (maxHeight != null) { + height = Math.min(height, maxHeight); } + changed += update(this, 'height', height); - return (changed > 0); + // calculate height from items + changed += update(this, 'top', frame.offsetTop); + changed += update(this, 'left', frame.offsetLeft); + changed += update(this, 'width', frame.offsetWidth); + } + else { + changed += 1; + } + + return (changed > 0); }; /** @@ -9216,19 +8676,19 @@ ItemSet.prototype.reflow = function reflow () { * @return {Boolean} changed */ ItemSet.prototype.hide = function hide() { - var changed = false; + var changed = false; - // remove the DOM - if (this.frame && this.frame.parentNode) { - this.frame.parentNode.removeChild(this.frame); - changed = true; - } - if (this.dom.axis && this.dom.axis.parentNode) { - this.dom.axis.parentNode.removeChild(this.dom.axis); - changed = true; - } + // remove the DOM + if (this.frame && this.frame.parentNode) { + this.frame.parentNode.removeChild(this.frame); + changed = true; + } + if (this.dom.axis && this.dom.axis.parentNode) { + this.dom.axis.parentNode.removeChild(this.dom.axis); + changed = true; + } - return changed; + return changed; }; /** @@ -9236,43 +8696,43 @@ ItemSet.prototype.hide = function hide() { * @param {vis.DataSet | null} items */ ItemSet.prototype.setItems = function setItems(items) { - var me = this, - ids, - oldItemsData = this.itemsData; + var me = this, + ids, + oldItemsData = this.itemsData; - // replace the dataset - if (!items) { - this.itemsData = null; - } - else if (items instanceof DataSet || items instanceof DataView) { - this.itemsData = items; - } - else { - throw new TypeError('Data must be an instance of DataSet'); - } + // replace the dataset + if (!items) { + this.itemsData = null; + } + else if (items instanceof DataSet || items instanceof DataView) { + this.itemsData = items; + } + else { + throw new TypeError('Data must be an instance of DataSet'); + } - if (oldItemsData) { - // unsubscribe from old dataset - util.forEach(this.listeners, function (callback, event) { - oldItemsData.unsubscribe(event, callback); - }); + if (oldItemsData) { + // unsubscribe from old dataset + util.forEach(this.listeners, function (callback, event) { + oldItemsData.unsubscribe(event, callback); + }); - // remove all drawn items - ids = oldItemsData.getIds(); - this._onRemove(ids); - } + // remove all drawn items + ids = oldItemsData.getIds(); + this._onRemove(ids); + } - if (this.itemsData) { - // subscribe to new dataset - var id = this.id; - util.forEach(this.listeners, function (callback, event) { - me.itemsData.subscribe(event, callback, id); - }); + if (this.itemsData) { + // subscribe to new dataset + var id = this.id; + util.forEach(this.listeners, function (callback, event) { + me.itemsData.subscribe(event, callback, id); + }); - // draw all new items - ids = this.itemsData.getIds(); - this._onAdd(ids); - } + // draw all new items + ids = this.itemsData.getIds(); + this._onAdd(ids); + } }; /** @@ -9280,7 +8740,7 @@ ItemSet.prototype.setItems = function setItems(items) { * @returns {vis.DataSet | null} */ ItemSet.prototype.getItems = function getItems() { - return this.itemsData; + return this.itemsData; }; /** @@ -9289,7 +8749,7 @@ ItemSet.prototype.getItems = function getItems() { * @private */ ItemSet.prototype._onUpdate = function _onUpdate(ids) { - this._toQueue('update', ids); + this._toQueue('update', ids); }; /** @@ -9298,7 +8758,7 @@ ItemSet.prototype._onUpdate = function _onUpdate(ids) { * @private */ ItemSet.prototype._onAdd = function _onAdd(ids) { - this._toQueue('add', ids); + this._toQueue('add', ids); }; /** @@ -9307,7 +8767,7 @@ ItemSet.prototype._onAdd = function _onAdd(ids) { * @private */ ItemSet.prototype._onRemove = function _onRemove(ids) { - this._toQueue('remove', ids); + this._toQueue('remove', ids); }; /** @@ -9316,36 +8776,36 @@ ItemSet.prototype._onRemove = function _onRemove(ids) { * @param {Number[]} ids */ ItemSet.prototype._toQueue = function _toQueue(action, ids) { - var queue = this.queue; - ids.forEach(function (id) { - queue[id] = action; - }); - - if (this.controller) { - //this.requestReflow(); - this.requestRepaint(); - } + var queue = this.queue; + ids.forEach(function (id) { + queue[id] = action; + }); + + if (this.controller) { + //this.requestReflow(); + this.requestRepaint(); + } }; /** - * Calculate the factor and offset to convert a position on screen to the + * Calculate the scale and offset to convert a position on screen to the * corresponding date and vice versa. * After the method _updateConversion is executed once, the methods toTime * and toScreen can be used. * @private */ ItemSet.prototype._updateConversion = function _updateConversion() { - var range = this.range; - if (!range) { - throw new Error('No range configured'); - } + var range = this.range; + if (!range) { + throw new Error('No range configured'); + } - if (range.conversion) { - this.conversion = range.conversion(this.width); - } - else { - this.conversion = Range.conversion(range.start, range.end, this.width); - } + if (range.conversion) { + this.conversion = range.conversion(this.width); + } + else { + this.conversion = Range.conversion(range.start, range.end, this.width); + } }; /** @@ -9356,8 +8816,8 @@ ItemSet.prototype._updateConversion = function _updateConversion() { * @return {Date} time The datetime the corresponds with given position x */ ItemSet.prototype.toTime = function toTime(x) { - var conversion = this.conversion; - return new Date(x / conversion.factor + conversion.offset); + var conversion = this.conversion; + return new Date(x / conversion.scale + conversion.offset); }; /** @@ -9369,8 +8829,8 @@ ItemSet.prototype.toTime = function toTime(x) { * with the given date. */ ItemSet.prototype.toScreen = function toScreen(time) { - var conversion = this.conversion; - return (time.valueOf() - conversion.offset) * conversion.factor; + var conversion = this.conversion; + return (time.valueOf() - conversion.offset) * conversion.scale; }; /** @@ -9383,32 +8843,32 @@ ItemSet.prototype.toScreen = function toScreen(time) { * // TODO: describe available options */ function Item (parent, data, options, defaultOptions) { - this.parent = parent; - this.data = data; - this.dom = null; - this.options = options || {}; - this.defaultOptions = defaultOptions || {}; - - this.selected = false; - this.visible = false; - this.top = 0; - this.left = 0; - this.width = 0; - this.height = 0; + this.parent = parent; + this.data = data; + this.dom = null; + this.options = options || {}; + this.defaultOptions = defaultOptions || {}; + + this.selected = false; + this.visible = false; + this.top = 0; + this.left = 0; + this.width = 0; + this.height = 0; } /** * Select current item */ Item.prototype.select = function select() { - this.selected = true; + this.selected = true; }; /** * Unselect current item */ Item.prototype.unselect = function unselect() { - this.selected = false; + this.selected = false; }; /** @@ -9416,7 +8876,7 @@ Item.prototype.unselect = function unselect() { * @return {Boolean} changed */ Item.prototype.show = function show() { - return false; + return false; }; /** @@ -9424,7 +8884,7 @@ Item.prototype.show = function show() { * @return {Boolean} changed */ Item.prototype.hide = function hide() { - return false; + return false; }; /** @@ -9432,8 +8892,8 @@ Item.prototype.hide = function hide() { * @return {Boolean} changed */ Item.prototype.repaint = function repaint() { - // should be implemented by the item - return false; + // should be implemented by the item + return false; }; /** @@ -9441,8 +8901,8 @@ Item.prototype.repaint = function repaint() { * @return {Boolean} resized */ Item.prototype.reflow = function reflow() { - // should be implemented by the item - return false; + // should be implemented by the item + return false; }; /** @@ -9450,7 +8910,7 @@ Item.prototype.reflow = function reflow() { * @return {Integer} width */ Item.prototype.getWidth = function getWidth() { - return this.width; + return this.width; } /** @@ -9464,22 +8924,22 @@ Item.prototype.getWidth = function getWidth() { * // TODO: describe available options */ function ItemBox (parent, data, options, defaultOptions) { - this.props = { - dot: { - left: 0, - top: 0, - width: 0, - height: 0 - }, - line: { - top: 0, - left: 0, - width: 0, - height: 0 - } - }; + this.props = { + dot: { + left: 0, + top: 0, + width: 0, + height: 0 + }, + line: { + top: 0, + left: 0, + width: 0, + height: 0 + } + }; - Item.call(this, parent, data, options, defaultOptions); + Item.call(this, parent, data, options, defaultOptions); } ItemBox.prototype = new Item (null, null); @@ -9489,8 +8949,8 @@ ItemBox.prototype = new Item (null, null); * @override */ ItemBox.prototype.select = function select() { - this.selected = true; - // TODO: select and unselect + this.selected = true; + // TODO: select and unselect }; /** @@ -9498,8 +8958,8 @@ ItemBox.prototype.select = function select() { * @override */ ItemBox.prototype.unselect = function unselect() { - this.selected = false; - // TODO: select and unselect + this.selected = false; + // TODO: select and unselect }; /** @@ -9507,78 +8967,80 @@ ItemBox.prototype.unselect = function unselect() { * @return {Boolean} changed */ ItemBox.prototype.repaint = function repaint() { - // TODO: make an efficient repaint - var changed = false; - var dom = this.dom; + // TODO: make an efficient repaint + var changed = false; + var dom = this.dom; + + if (!dom) { + this._create(); + dom = this.dom; + changed = true; + } - if (!dom) { - this._create(); - dom = this.dom; - changed = true; + if (dom) { + if (!this.parent) { + throw new Error('Cannot repaint item: no parent attached'); } - if (dom) { - if (!this.parent) { - throw new Error('Cannot repaint item: no parent attached'); - } - var foreground = this.parent.getForeground(); - if (!foreground) { - throw new Error('Cannot repaint time axis: ' + - 'parent has no foreground container element'); - } - var background = this.parent.getBackground(); - if (!background) { - throw new Error('Cannot repaint time axis: ' + - 'parent has no background container element'); - } - var axis = this.parent.getAxis(); - if (!background) { - throw new Error('Cannot repaint time axis: ' + - 'parent has no axis container element'); - } + if (!dom.box.parentNode) { + var foreground = this.parent.getForeground(); + if (!foreground) { + throw new Error('Cannot repaint time axis: ' + + 'parent has no foreground container element'); + } + foreground.appendChild(dom.box); + changed = true; + } - if (!dom.box.parentNode) { - foreground.appendChild(dom.box); - changed = true; - } - if (!dom.line.parentNode) { - background.appendChild(dom.line); - changed = true; - } - if (!dom.dot.parentNode) { - axis.appendChild(dom.dot); - changed = true; - } + if (!dom.line.parentNode) { + var background = this.parent.getBackground(); + if (!background) { + throw new Error('Cannot repaint time axis: ' + + 'parent has no background container element'); + } + background.appendChild(dom.line); + changed = true; + } - // update contents - if (this.data.content != this.content) { - this.content = this.data.content; - if (this.content instanceof Element) { - dom.content.innerHTML = ''; - dom.content.appendChild(this.content); - } - else if (this.data.content != undefined) { - dom.content.innerHTML = this.content; - } - else { - throw new Error('Property "content" missing in item ' + this.data.id); - } - changed = true; - } + if (!dom.dot.parentNode) { + var axis = this.parent.getAxis(); + if (!background) { + throw new Error('Cannot repaint time axis: ' + + 'parent has no axis container element'); + } + axis.appendChild(dom.dot); + changed = true; + } - // update class - var className = (this.data.className? ' ' + this.data.className : '') + - (this.selected ? ' selected' : ''); - if (this.className != className) { - this.className = className; - dom.box.className = 'item box' + className; - dom.line.className = 'item line' + className; - dom.dot.className = 'item dot' + className; - changed = true; - } + // update contents + if (this.data.content != this.content) { + this.content = this.data.content; + if (this.content instanceof Element) { + dom.content.innerHTML = ''; + dom.content.appendChild(this.content); + } + else if (this.data.content != undefined) { + dom.content.innerHTML = this.content; + } + else { + throw new Error('Property "content" missing in item ' + this.data.id); + } + changed = true; } - return changed; + // update class + var className = (this.data.className? ' ' + this.data.className : '') + + (this.selected ? ' selected' : ''); + if (this.className != className) { + this.className = className; + dom.box.className = 'item box' + className; + dom.line.className = 'item line' + className; + dom.dot.className = 'item dot' + className; + changed = true; + } + } + + return changed; }; /** @@ -9587,12 +9049,12 @@ ItemBox.prototype.repaint = function repaint() { * @return {Boolean} changed */ ItemBox.prototype.show = function show() { - if (!this.dom || !this.dom.box.parentNode) { - return this.repaint(); - } - else { - return false; - } + if (!this.dom || !this.dom.box.parentNode) { + return this.repaint(); + } + else { + return false; + } }; /** @@ -9600,21 +9062,21 @@ ItemBox.prototype.show = function show() { * @return {Boolean} changed */ ItemBox.prototype.hide = function hide() { - var changed = false, - dom = this.dom; - if (dom) { - if (dom.box.parentNode) { - dom.box.parentNode.removeChild(dom.box); - changed = true; - } - if (dom.line.parentNode) { - dom.line.parentNode.removeChild(dom.line); - } - if (dom.dot.parentNode) { - dom.dot.parentNode.removeChild(dom.dot); - } + var changed = false, + dom = this.dom; + if (dom) { + if (dom.box.parentNode) { + dom.box.parentNode.removeChild(dom.box); + changed = true; } - return changed; + if (dom.line.parentNode) { + dom.line.parentNode.removeChild(dom.line); + } + if (dom.dot.parentNode) { + dom.dot.parentNode.removeChild(dom.dot); + } + } + return changed; }; /** @@ -9623,87 +9085,87 @@ ItemBox.prototype.hide = function hide() { * @override */ ItemBox.prototype.reflow = function reflow() { - var changed = 0, - update, - dom, - props, - options, - margin, - start, - align, - orientation, - top, - left, - data, - range; - - if (this.data.start == undefined) { - throw new Error('Property "start" missing in item ' + this.data.id); - } - - data = this.data; - range = this.parent && this.parent.range; - if (data && range) { - // TODO: account for the width of the item - var interval = (range.end - range.start); - this.visible = (data.start > range.start - interval) && (data.start < range.end + interval); - } - else { - this.visible = false; - } - - if (this.visible) { - dom = this.dom; - if (dom) { - update = util.updateProperty; - props = this.props; - options = this.options; - start = this.parent.toScreen(this.data.start); - align = options.align || this.defaultOptions.align; - margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis; - orientation = options.orientation || this.defaultOptions.orientation; - - changed += update(props.dot, 'height', dom.dot.offsetHeight); - changed += update(props.dot, 'width', dom.dot.offsetWidth); - changed += update(props.line, 'width', dom.line.offsetWidth); - changed += update(props.line, 'height', dom.line.offsetHeight); - changed += update(props.line, 'top', dom.line.offsetTop); - changed += update(this, 'width', dom.box.offsetWidth); - changed += update(this, 'height', dom.box.offsetHeight); - if (align == 'right') { - left = start - this.width; - } - else if (align == 'left') { - left = start; - } - else { - // default or 'center' - left = start - this.width / 2; - } - changed += update(this, 'left', left); + var changed = 0, + update, + dom, + props, + options, + margin, + start, + align, + orientation, + top, + left, + data, + range; + + if (this.data.start == undefined) { + throw new Error('Property "start" missing in item ' + this.data.id); + } - changed += update(props.line, 'left', start - props.line.width / 2); - changed += update(props.dot, 'left', start - props.dot.width / 2); - changed += update(props.dot, 'top', -props.dot.height / 2); - if (orientation == 'top') { - top = margin; + data = this.data; + range = this.parent && this.parent.range; + if (data && range) { + // TODO: account for the width of the item + var interval = (range.end - range.start); + this.visible = (data.start > range.start - interval) && (data.start < range.end + interval); + } + else { + this.visible = false; + } - changed += update(this, 'top', top); - } - else { - // default or 'bottom' - var parentHeight = this.parent.height; - top = parentHeight - this.height - margin; + if (this.visible) { + dom = this.dom; + if (dom) { + update = util.updateProperty; + props = this.props; + options = this.options; + start = this.parent.toScreen(this.data.start); + align = options.align || this.defaultOptions.align; + margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis; + orientation = options.orientation || this.defaultOptions.orientation; + + changed += update(props.dot, 'height', dom.dot.offsetHeight); + changed += update(props.dot, 'width', dom.dot.offsetWidth); + changed += update(props.line, 'width', dom.line.offsetWidth); + changed += update(props.line, 'height', dom.line.offsetHeight); + changed += update(props.line, 'top', dom.line.offsetTop); + changed += update(this, 'width', dom.box.offsetWidth); + changed += update(this, 'height', dom.box.offsetHeight); + if (align == 'right') { + left = start - this.width; + } + else if (align == 'left') { + left = start; + } + else { + // default or 'center' + left = start - this.width / 2; + } + changed += update(this, 'left', left); - changed += update(this, 'top', top); - } - } - else { - changed += 1; - } + changed += update(props.line, 'left', start - props.line.width / 2); + changed += update(props.dot, 'left', start - props.dot.width / 2); + changed += update(props.dot, 'top', -props.dot.height / 2); + if (orientation == 'top') { + top = margin; + + changed += update(this, 'top', top); + } + else { + // default or 'bottom' + var parentHeight = this.parent.height; + top = parentHeight - this.height - margin; + + changed += update(this, 'top', top); + } + } + else { + changed += 1; } + } - return (changed > 0); + return (changed > 0); }; /** @@ -9711,27 +9173,27 @@ ItemBox.prototype.reflow = function reflow() { * @private */ ItemBox.prototype._create = function _create() { - var dom = this.dom; - if (!dom) { - this.dom = dom = {}; - - // create the box - dom.box = document.createElement('DIV'); - // className is updated in repaint() - - // contents box (inside the background box). used for making margins - dom.content = document.createElement('DIV'); - dom.content.className = 'content'; - dom.box.appendChild(dom.content); - - // line to axis - dom.line = document.createElement('DIV'); - dom.line.className = 'line'; - - // dot on axis - dom.dot = document.createElement('DIV'); - dom.dot.className = 'dot'; - } + var dom = this.dom; + if (!dom) { + this.dom = dom = {}; + + // create the box + dom.box = document.createElement('DIV'); + // className is updated in repaint() + + // contents box (inside the background box). used for making margins + dom.content = document.createElement('DIV'); + dom.content.className = 'content'; + dom.box.appendChild(dom.content); + + // line to axis + dom.line = document.createElement('DIV'); + dom.line.className = 'line'; + + // dot on axis + dom.dot = document.createElement('DIV'); + dom.dot.className = 'dot'; + } }; /** @@ -9740,33 +9202,33 @@ ItemBox.prototype._create = function _create() { * @override */ ItemBox.prototype.reposition = function reposition() { - var dom = this.dom, - props = this.props, - orientation = this.options.orientation || this.defaultOptions.orientation; - - if (dom) { - var box = dom.box, - line = dom.line, - dot = dom.dot; + var dom = this.dom, + props = this.props, + orientation = this.options.orientation || this.defaultOptions.orientation; - box.style.left = this.left + 'px'; - box.style.top = this.top + 'px'; + if (dom) { + var box = dom.box, + line = dom.line, + dot = dom.dot; - line.style.left = props.line.left + 'px'; - if (orientation == 'top') { - line.style.top = 0 + 'px'; - line.style.height = this.top + 'px'; - } - else { - // orientation 'bottom' - line.style.top = (this.top + this.height) + 'px'; - line.style.height = Math.max(this.parent.height - this.top - this.height + - this.props.dot.height / 2, 0) + 'px'; - } + box.style.left = this.left + 'px'; + box.style.top = this.top + 'px'; - dot.style.left = props.dot.left + 'px'; - dot.style.top = props.dot.top + 'px'; + line.style.left = props.line.left + 'px'; + if (orientation == 'top') { + line.style.top = 0 + 'px'; + line.style.height = this.top + 'px'; } + else { + // orientation 'bottom' + line.style.top = (this.top + this.height) + 'px'; + line.style.height = Math.max(this.parent.height - this.top - this.height + + this.props.dot.height / 2, 0) + 'px'; + } + + dot.style.left = props.dot.left + 'px'; + dot.style.top = props.dot.top + 'px'; + } }; /** @@ -9780,19 +9242,19 @@ ItemBox.prototype.reposition = function reposition() { * // TODO: describe available options */ function ItemPoint (parent, data, options, defaultOptions) { - this.props = { - dot: { - top: 0, - width: 0, - height: 0 - }, - content: { - height: 0, - marginLeft: 0 - } - }; + this.props = { + dot: { + top: 0, + width: 0, + height: 0 + }, + content: { + height: 0, + marginLeft: 0 + } + }; - Item.call(this, parent, data, options, defaultOptions); + Item.call(this, parent, data, options, defaultOptions); } ItemPoint.prototype = new Item (null, null); @@ -9802,8 +9264,8 @@ ItemPoint.prototype = new Item (null, null); * @override */ ItemPoint.prototype.select = function select() { - this.selected = true; - // TODO: select and unselect + this.selected = true; + // TODO: select and unselect }; /** @@ -9811,8 +9273,8 @@ ItemPoint.prototype.select = function select() { * @override */ ItemPoint.prototype.unselect = function unselect() { - this.selected = false; - // TODO: select and unselect + this.selected = false; + // TODO: select and unselect }; /** @@ -9820,59 +9282,59 @@ ItemPoint.prototype.unselect = function unselect() { * @return {Boolean} changed */ ItemPoint.prototype.repaint = function repaint() { - // TODO: make an efficient repaint - var changed = false; - var dom = this.dom; + // TODO: make an efficient repaint + var changed = false; + var dom = this.dom; - if (!dom) { - this._create(); - dom = this.dom; - changed = true; - } + if (!dom) { + this._create(); + dom = this.dom; + changed = true; + } - if (dom) { - if (!this.parent) { - throw new Error('Cannot repaint item: no parent attached'); - } - var foreground = this.parent.getForeground(); - if (!foreground) { - throw new Error('Cannot repaint time axis: ' + - 'parent has no foreground container element'); - } + if (dom) { + if (!this.parent) { + throw new Error('Cannot repaint item: no parent attached'); + } + var foreground = this.parent.getForeground(); + if (!foreground) { + throw new Error('Cannot repaint time axis: ' + + 'parent has no foreground container element'); + } - if (!dom.point.parentNode) { - foreground.appendChild(dom.point); - foreground.appendChild(dom.point); - changed = true; - } + if (!dom.point.parentNode) { + foreground.appendChild(dom.point); + foreground.appendChild(dom.point); + changed = true; + } - // update contents - if (this.data.content != this.content) { - this.content = this.data.content; - if (this.content instanceof Element) { - dom.content.innerHTML = ''; - dom.content.appendChild(this.content); - } - else if (this.data.content != undefined) { - dom.content.innerHTML = this.content; - } - else { - throw new Error('Property "content" missing in item ' + this.data.id); - } - changed = true; - } + // update contents + if (this.data.content != this.content) { + this.content = this.data.content; + if (this.content instanceof Element) { + dom.content.innerHTML = ''; + dom.content.appendChild(this.content); + } + else if (this.data.content != undefined) { + dom.content.innerHTML = this.content; + } + else { + throw new Error('Property "content" missing in item ' + this.data.id); + } + changed = true; + } - // update class - var className = (this.data.className? ' ' + this.data.className : '') + - (this.selected ? ' selected' : ''); - if (this.className != className) { - this.className = className; - dom.point.className = 'item point' + className; - changed = true; - } + // update class + var className = (this.data.className? ' ' + this.data.className : '') + + (this.selected ? ' selected' : ''); + if (this.className != className) { + this.className = className; + dom.point.className = 'item point' + className; + changed = true; } + } - return changed; + return changed; }; /** @@ -9881,12 +9343,12 @@ ItemPoint.prototype.repaint = function repaint() { * @return {Boolean} changed */ ItemPoint.prototype.show = function show() { - if (!this.dom || !this.dom.point.parentNode) { - return this.repaint(); - } - else { - return false; - } + if (!this.dom || !this.dom.point.parentNode) { + return this.repaint(); + } + else { + return false; + } }; /** @@ -9894,15 +9356,15 @@ ItemPoint.prototype.show = function show() { * @return {Boolean} changed */ ItemPoint.prototype.hide = function hide() { - var changed = false, - dom = this.dom; - if (dom) { - if (dom.point.parentNode) { - dom.point.parentNode.removeChild(dom.point); - changed = true; - } + var changed = false, + dom = this.dom; + if (dom) { + if (dom.point.parentNode) { + dom.point.parentNode.removeChild(dom.point); + changed = true; } - return changed; + } + return changed; }; /** @@ -9911,70 +9373,70 @@ ItemPoint.prototype.hide = function hide() { * @override */ ItemPoint.prototype.reflow = function reflow() { - var changed = 0, - update, - dom, - props, - options, - margin, - orientation, - start, - top, - data, - range; - - if (this.data.start == undefined) { - throw new Error('Property "start" missing in item ' + this.data.id); - } - - data = this.data; - range = this.parent && this.parent.range; - if (data && range) { - // TODO: account for the width of the item - var interval = (range.end - range.start); - this.visible = (data.start > range.start - interval) && (data.start < range.end); + var changed = 0, + update, + dom, + props, + options, + margin, + orientation, + start, + top, + data, + range; + + if (this.data.start == undefined) { + throw new Error('Property "start" missing in item ' + this.data.id); + } + + data = this.data; + range = this.parent && this.parent.range; + if (data && range) { + // TODO: account for the width of the item + var interval = (range.end - range.start); + this.visible = (data.start > range.start - interval) && (data.start < range.end); + } + else { + this.visible = false; + } + + if (this.visible) { + dom = this.dom; + if (dom) { + update = util.updateProperty; + props = this.props; + options = this.options; + orientation = options.orientation || this.defaultOptions.orientation; + margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis; + start = this.parent.toScreen(this.data.start); + + changed += update(this, 'width', dom.point.offsetWidth); + changed += update(this, 'height', dom.point.offsetHeight); + changed += update(props.dot, 'width', dom.dot.offsetWidth); + changed += update(props.dot, 'height', dom.dot.offsetHeight); + changed += update(props.content, 'height', dom.content.offsetHeight); + + if (orientation == 'top') { + top = margin; + } + else { + // default or 'bottom' + var parentHeight = this.parent.height; + top = Math.max(parentHeight - this.height - margin, 0); + } + changed += update(this, 'top', top); + changed += update(this, 'left', start - props.dot.width / 2); + changed += update(props.content, 'marginLeft', 1.5 * props.dot.width); + //changed += update(props.content, 'marginRight', 0.5 * props.dot.width); // TODO + + changed += update(props.dot, 'top', (this.height - props.dot.height) / 2); } else { - this.visible = false; - } - - if (this.visible) { - dom = this.dom; - if (dom) { - update = util.updateProperty; - props = this.props; - options = this.options; - orientation = options.orientation || this.defaultOptions.orientation; - margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis; - start = this.parent.toScreen(this.data.start); - - changed += update(this, 'width', dom.point.offsetWidth); - changed += update(this, 'height', dom.point.offsetHeight); - changed += update(props.dot, 'width', dom.dot.offsetWidth); - changed += update(props.dot, 'height', dom.dot.offsetHeight); - changed += update(props.content, 'height', dom.content.offsetHeight); - - if (orientation == 'top') { - top = margin; - } - else { - // default or 'bottom' - var parentHeight = this.parent.height; - top = Math.max(parentHeight - this.height - margin, 0); - } - changed += update(this, 'top', top); - changed += update(this, 'left', start - props.dot.width / 2); - changed += update(props.content, 'marginLeft', 1.5 * props.dot.width); - //changed += update(props.content, 'marginRight', 0.5 * props.dot.width); // TODO - - changed += update(props.dot, 'top', (this.height - props.dot.height) / 2); - } - else { - changed += 1; - } + changed += 1; } + } - return (changed > 0); + return (changed > 0); }; /** @@ -9982,24 +9444,24 @@ ItemPoint.prototype.reflow = function reflow() { * @private */ ItemPoint.prototype._create = function _create() { - var dom = this.dom; - if (!dom) { - this.dom = dom = {}; - - // background box - dom.point = document.createElement('div'); - // className is updated in repaint() - - // contents box, right from the dot - dom.content = document.createElement('div'); - dom.content.className = 'content'; - dom.point.appendChild(dom.content); - - // dot at start - dom.dot = document.createElement('div'); - dom.dot.className = 'dot'; - dom.point.appendChild(dom.dot); - } + var dom = this.dom; + if (!dom) { + this.dom = dom = {}; + + // background box + dom.point = document.createElement('div'); + // className is updated in repaint() + + // contents box, right from the dot + dom.content = document.createElement('div'); + dom.content.className = 'content'; + dom.point.appendChild(dom.content); + + // dot at start + dom.dot = document.createElement('div'); + dom.dot.className = 'dot'; + dom.point.appendChild(dom.dot); + } }; /** @@ -10008,18 +9470,18 @@ ItemPoint.prototype._create = function _create() { * @override */ ItemPoint.prototype.reposition = function reposition() { - var dom = this.dom, - props = this.props; + var dom = this.dom, + props = this.props; - if (dom) { - dom.point.style.top = this.top + 'px'; - dom.point.style.left = this.left + 'px'; + if (dom) { + dom.point.style.top = this.top + 'px'; + dom.point.style.left = this.left + 'px'; - dom.content.style.marginLeft = props.content.marginLeft + 'px'; - //dom.content.style.marginRight = props.content.marginRight + 'px'; // TODO + dom.content.style.marginLeft = props.content.marginLeft + 'px'; + //dom.content.style.marginRight = props.content.marginRight + 'px'; // TODO - dom.dot.style.top = props.dot.top + 'px'; - } + dom.dot.style.top = props.dot.top + 'px'; + } }; /** @@ -10033,14 +9495,14 @@ ItemPoint.prototype.reposition = function reposition() { * // TODO: describe available options */ function ItemRange (parent, data, options, defaultOptions) { - this.props = { - content: { - left: 0, - width: 0 - } - }; + this.props = { + content: { + left: 0, + width: 0 + } + }; - Item.call(this, parent, data, options, defaultOptions); + Item.call(this, parent, data, options, defaultOptions); } ItemRange.prototype = new Item (null, null); @@ -10050,8 +9512,8 @@ ItemRange.prototype = new Item (null, null); * @override */ ItemRange.prototype.select = function select() { - this.selected = true; - // TODO: select and unselect + this.selected = true; + // TODO: select and unselect }; /** @@ -10059,8 +9521,8 @@ ItemRange.prototype.select = function select() { * @override */ ItemRange.prototype.unselect = function unselect() { - this.selected = false; - // TODO: select and unselect + this.selected = false; + // TODO: select and unselect }; /** @@ -10068,57 +9530,57 @@ ItemRange.prototype.unselect = function unselect() { * @return {Boolean} changed */ ItemRange.prototype.repaint = function repaint() { - // TODO: make an efficient repaint - var changed = false; - var dom = this.dom; + // TODO: make an efficient repaint + var changed = false; + var dom = this.dom; - if (!dom) { - this._create(); - dom = this.dom; - changed = true; - } + if (!dom) { + this._create(); + dom = this.dom; + changed = true; + } - if (dom) { - if (!this.parent) { - throw new Error('Cannot repaint item: no parent attached'); - } - var foreground = this.parent.getForeground(); - if (!foreground) { - throw new Error('Cannot repaint time axis: ' + - 'parent has no foreground container element'); - } + if (dom) { + if (!this.parent) { + throw new Error('Cannot repaint item: no parent attached'); + } + var foreground = this.parent.getForeground(); + if (!foreground) { + throw new Error('Cannot repaint time axis: ' + + 'parent has no foreground container element'); + } - if (!dom.box.parentNode) { - foreground.appendChild(dom.box); - changed = true; - } + if (!dom.box.parentNode) { + foreground.appendChild(dom.box); + changed = true; + } - // update content - if (this.data.content != this.content) { - this.content = this.data.content; - if (this.content instanceof Element) { - dom.content.innerHTML = ''; - dom.content.appendChild(this.content); - } - else if (this.data.content != undefined) { - dom.content.innerHTML = this.content; - } - else { - throw new Error('Property "content" missing in item ' + this.data.id); - } - changed = true; - } + // update content + if (this.data.content != this.content) { + this.content = this.data.content; + if (this.content instanceof Element) { + dom.content.innerHTML = ''; + dom.content.appendChild(this.content); + } + else if (this.data.content != undefined) { + dom.content.innerHTML = this.content; + } + else { + throw new Error('Property "content" missing in item ' + this.data.id); + } + changed = true; + } - // update class - var className = this.data.className ? (' ' + this.data.className) : ''; - if (this.className != className) { - this.className = className; - dom.box.className = 'item range' + className; - changed = true; - } + // update class + var className = this.data.className ? (' ' + this.data.className) : ''; + if (this.className != className) { + this.className = className; + dom.box.className = 'item range' + className; + changed = true; } + } - return changed; + return changed; }; /** @@ -10127,12 +9589,12 @@ ItemRange.prototype.repaint = function repaint() { * @return {Boolean} changed */ ItemRange.prototype.show = function show() { - if (!this.dom || !this.dom.box.parentNode) { - return this.repaint(); - } - else { - return false; - } + if (!this.dom || !this.dom.box.parentNode) { + return this.repaint(); + } + else { + return false; + } }; /** @@ -10140,15 +9602,15 @@ ItemRange.prototype.show = function show() { * @return {Boolean} changed */ ItemRange.prototype.hide = function hide() { - var changed = false, - dom = this.dom; - if (dom) { - if (dom.box.parentNode) { - dom.box.parentNode.removeChild(dom.box); - changed = true; - } + var changed = false, + dom = this.dom; + if (dom) { + if (dom.box.parentNode) { + dom.box.parentNode.removeChild(dom.box); + changed = true; } - return changed; + } + return changed; }; /** @@ -10157,98 +9619,98 @@ ItemRange.prototype.hide = function hide() { * @override */ ItemRange.prototype.reflow = function reflow() { - var changed = 0, - dom, - props, - options, - margin, - padding, - parent, - start, - end, - data, - range, - update, - box, - parentWidth, - contentLeft, - orientation, - top; - - if (this.data.start == undefined) { - throw new Error('Property "start" missing in item ' + this.data.id); - } - if (this.data.end == undefined) { - throw new Error('Property "end" missing in item ' + this.data.id); - } - - data = this.data; - range = this.parent && this.parent.range; - if (data && range) { - // TODO: account for the width of the item. Take some margin - this.visible = (data.start < range.end) && (data.end > range.start); - } - else { - this.visible = false; - } - - if (this.visible) { - dom = this.dom; - if (dom) { - props = this.props; - options = this.options; - parent = this.parent; - start = parent.toScreen(this.data.start); - end = parent.toScreen(this.data.end); - update = util.updateProperty; - box = dom.box; - parentWidth = parent.width; - orientation = options.orientation || this.defaultOptions.orientation; - margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis; - padding = options.padding || this.defaultOptions.padding; - - changed += update(props.content, 'width', dom.content.offsetWidth); - - changed += update(this, 'height', box.offsetHeight); - - // limit the width of the this, as browsers cannot draw very wide divs - if (start < -parentWidth) { - start = -parentWidth; - } - if (end > 2 * parentWidth) { - end = 2 * parentWidth; - } + var changed = 0, + dom, + props, + options, + margin, + padding, + parent, + start, + end, + data, + range, + update, + box, + parentWidth, + contentLeft, + orientation, + top; + + if (this.data.start == undefined) { + throw new Error('Property "start" missing in item ' + this.data.id); + } + if (this.data.end == undefined) { + throw new Error('Property "end" missing in item ' + this.data.id); + } - // when range exceeds left of the window, position the contents at the left of the visible area - if (start < 0) { - contentLeft = Math.min(-start, - (end - start - props.content.width - 2 * padding)); - // TODO: remove the need for options.padding. it's terrible. - } - else { - contentLeft = 0; - } - changed += update(props.content, 'left', contentLeft); + data = this.data; + range = this.parent && this.parent.range; + if (data && range) { + // TODO: account for the width of the item. Take some margin + this.visible = (data.start < range.end) && (data.end > range.start); + } + else { + this.visible = false; + } - if (orientation == 'top') { - top = margin; - changed += update(this, 'top', top); - } - else { - // default or 'bottom' - top = parent.height - this.height - margin; - changed += update(this, 'top', top); - } + if (this.visible) { + dom = this.dom; + if (dom) { + props = this.props; + options = this.options; + parent = this.parent; + start = parent.toScreen(this.data.start); + end = parent.toScreen(this.data.end); + update = util.updateProperty; + box = dom.box; + parentWidth = parent.width; + orientation = options.orientation || this.defaultOptions.orientation; + margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis; + padding = options.padding || this.defaultOptions.padding; + + changed += update(props.content, 'width', dom.content.offsetWidth); + + changed += update(this, 'height', box.offsetHeight); + + // limit the width of the this, as browsers cannot draw very wide divs + if (start < -parentWidth) { + start = -parentWidth; + } + if (end > 2 * parentWidth) { + end = 2 * parentWidth; + } - changed += update(this, 'left', start); - changed += update(this, 'width', Math.max(end - start, 1)); // TODO: reckon with border width; - } - else { - changed += 1; - } + // when range exceeds left of the window, position the contents at the left of the visible area + if (start < 0) { + contentLeft = Math.min(-start, + (end - start - props.content.width - 2 * padding)); + // TODO: remove the need for options.padding. it's terrible. + } + else { + contentLeft = 0; + } + changed += update(props.content, 'left', contentLeft); + + if (orientation == 'top') { + top = margin; + changed += update(this, 'top', top); + } + else { + // default or 'bottom' + top = parent.height - this.height - margin; + changed += update(this, 'top', top); + } + + changed += update(this, 'left', start); + changed += update(this, 'width', Math.max(end - start, 1)); // TODO: reckon with border width; + } + else { + changed += 1; } + } - return (changed > 0); + return (changed > 0); }; /** @@ -10256,18 +9718,18 @@ ItemRange.prototype.reflow = function reflow() { * @private */ ItemRange.prototype._create = function _create() { - var dom = this.dom; - if (!dom) { - this.dom = dom = {}; - // background box - dom.box = document.createElement('div'); - // className is updated in repaint() - - // contents box - dom.content = document.createElement('div'); - dom.content.className = 'content'; - dom.box.appendChild(dom.content); - } + var dom = this.dom; + if (!dom) { + this.dom = dom = {}; + // background box + dom.box = document.createElement('div'); + // className is updated in repaint() + + // contents box + dom.content = document.createElement('div'); + dom.content.className = 'content'; + dom.box.appendChild(dom.content); + } }; /** @@ -10276,16 +9738,16 @@ ItemRange.prototype._create = function _create() { * @override */ ItemRange.prototype.reposition = function reposition() { - var dom = this.dom, - props = this.props; + var dom = this.dom, + props = this.props; - if (dom) { - dom.box.style.top = this.top + 'px'; - dom.box.style.left = this.left + 'px'; - dom.box.style.width = this.width + 'px'; + if (dom) { + dom.box.style.top = this.top + 'px'; + dom.box.style.left = this.left + 'px'; + dom.box.style.width = this.width + 'px'; - dom.content.style.left = props.content.left + 'px'; - } + dom.content.style.left = props.content.left + 'px'; + } }; /** @@ -10299,14 +9761,14 @@ ItemRange.prototype.reposition = function reposition() { * // TODO: describe available options */ function ItemRangeOverflow (parent, data, options, defaultOptions) { - this.props = { - content: { - left: 0, - width: 0 - } - }; + this.props = { + content: { + left: 0, + width: 0 + } + }; - ItemRange.call(this, parent, data, options, defaultOptions); + ItemRange.call(this, parent, data, options, defaultOptions); } ItemRangeOverflow.prototype = new ItemRange (null, null); @@ -10316,69 +9778,69 @@ ItemRangeOverflow.prototype = new ItemRange (null, null); * @return {Boolean} changed */ ItemRangeOverflow.prototype.repaint = function repaint() { - // TODO: make an efficient repaint - var changed = false; - var dom = this.dom; + // TODO: make an efficient repaint + var changed = false; + var dom = this.dom; - if (!dom) { - this._create(); - dom = this.dom; - changed = true; - } + if (!dom) { + this._create(); + dom = this.dom; + changed = true; + } - if (dom) { - if (!this.parent) { - throw new Error('Cannot repaint item: no parent attached'); - } - var foreground = this.parent.getForeground(); - if (!foreground) { - throw new Error('Cannot repaint time axis: ' + - 'parent has no foreground container element'); - } + if (dom) { + if (!this.parent) { + throw new Error('Cannot repaint item: no parent attached'); + } + var foreground = this.parent.getForeground(); + if (!foreground) { + throw new Error('Cannot repaint time axis: ' + + 'parent has no foreground container element'); + } - if (!dom.box.parentNode) { - foreground.appendChild(dom.box); - changed = true; - } + if (!dom.box.parentNode) { + foreground.appendChild(dom.box); + changed = true; + } - // update content - if (this.data.content != this.content) { - this.content = this.data.content; - if (this.content instanceof Element) { - dom.content.innerHTML = ''; - dom.content.appendChild(this.content); - } - else if (this.data.content != undefined) { - dom.content.innerHTML = this.content; - } - else { - throw new Error('Property "content" missing in item ' + this.data.id); - } - changed = true; - } + // update content + if (this.data.content != this.content) { + this.content = this.data.content; + if (this.content instanceof Element) { + dom.content.innerHTML = ''; + dom.content.appendChild(this.content); + } + else if (this.data.content != undefined) { + dom.content.innerHTML = this.content; + } + else { + throw new Error('Property "content" missing in item ' + this.data.id); + } + changed = true; + } - // update class - var className = this.data.className ? (' ' + this.data.className) : ''; - if (this.className != className) { - this.className = className; - dom.box.className = 'item rangeoverflow' + className; - changed = true; - } + // update class + var className = this.data.className ? (' ' + this.data.className) : ''; + if (this.className != className) { + this.className = className; + dom.box.className = 'item rangeoverflow' + className; + changed = true; } + } - return changed; + return changed; }; /** * Return the items width - * @return {Integer} width + * @return {Number} width */ ItemRangeOverflow.prototype.getWidth = function getWidth() { - if (this.props.content !== undefined && this.width < this.props.content.width) - return this.props.content.width; - else - return this.width; -} + if (this.props.content !== undefined && this.width < this.props.content.width) + return this.props.content.width; + else + return this.width; +}; /** * @constructor Group @@ -10389,25 +9851,25 @@ ItemRangeOverflow.prototype.getWidth = function getWidth() { * @extends Component */ function Group (parent, groupId, options) { - this.id = util.randomUUID(); - this.parent = parent; + this.id = util.randomUUID(); + this.parent = parent; - this.groupId = groupId; - this.itemset = null; // ItemSet - this.options = options || {}; - this.options.top = 0; + this.groupId = groupId; + this.itemset = null; // ItemSet + this.options = options || {}; + this.options.top = 0; - this.props = { - label: { - width: 0, - height: 0 - } - }; + this.props = { + label: { + width: 0, + height: 0 + } + }; - this.top = 0; - this.left = 0; - this.width = 0; - this.height = 0; + this.top = 0; + this.left = 0; + this.width = 0; + this.height = 0; } Group.prototype = new Component(); @@ -10421,7 +9883,7 @@ Group.prototype.setOptions = Component.prototype.setOptions; * @returns {HTMLElement} container */ Group.prototype.getContainer = function () { - return this.parent.getContainer(); + return this.parent.getContainer(); }; /** @@ -10430,31 +9892,31 @@ Group.prototype.getContainer = function () { * @param {DataSet | DataView} items */ Group.prototype.setItems = function setItems(items) { - if (this.itemset) { - // remove current item set - this.itemset.hide(); - this.itemset.setItems(); + if (this.itemset) { + // remove current item set + this.itemset.hide(); + this.itemset.setItems(); - this.parent.controller.remove(this.itemset); - this.itemset = null; - } + this.parent.controller.remove(this.itemset); + this.itemset = null; + } - if (items) { - var groupId = this.groupId; + if (items) { + var groupId = this.groupId; - var itemsetOptions = Object.create(this.options); - this.itemset = new ItemSet(this, null, itemsetOptions); - this.itemset.setRange(this.parent.range); + var itemsetOptions = Object.create(this.options); + this.itemset = new ItemSet(this, null, itemsetOptions); + this.itemset.setRange(this.parent.range); - this.view = new DataView(items, { - filter: function (item) { - return item.group == groupId; - } - }); - this.itemset.setItems(this.view); + this.view = new DataView(items, { + filter: function (item) { + return item.group == groupId; + } + }); + this.itemset.setItems(this.view); - this.parent.controller.add(this.itemset); - } + this.parent.controller.add(this.itemset); + } }; /** @@ -10462,7 +9924,7 @@ Group.prototype.setItems = function setItems(items) { * @return {Boolean} changed */ Group.prototype.repaint = function repaint() { - return false; + return false; }; /** @@ -10470,25 +9932,25 @@ Group.prototype.repaint = function repaint() { * @return {Boolean} resized */ Group.prototype.reflow = function reflow() { - var changed = 0, - update = util.updateProperty; + var changed = 0, + update = util.updateProperty; - changed += update(this, 'top', this.itemset ? this.itemset.top : 0); - changed += update(this, 'height', this.itemset ? this.itemset.height : 0); + changed += update(this, 'top', this.itemset ? this.itemset.top : 0); + changed += update(this, 'height', this.itemset ? this.itemset.height : 0); - // TODO: reckon with the height of the group label + // TODO: reckon with the height of the group label - if (this.label) { - var inner = this.label.firstChild; - changed += update(this.props.label, 'width', inner.clientWidth); - changed += update(this.props.label, 'height', inner.clientHeight); - } - else { - changed += update(this.props.label, 'width', 0); - changed += update(this.props.label, 'height', 0); - } + if (this.label) { + var inner = this.label.firstChild; + changed += update(this.props.label, 'width', inner.clientWidth); + changed += update(this.props.label, 'height', inner.clientHeight); + } + else { + changed += update(this.props.label, 'width', 0); + changed += update(this.props.label, 'height', 0); + } - return (changed > 0); + return (changed > 0); }; /** @@ -10502,42 +9964,42 @@ Group.prototype.reflow = function reflow() { * @extends Panel */ function GroupSet(parent, depends, options) { - this.id = util.randomUUID(); - this.parent = parent; - this.depends = depends; + this.id = util.randomUUID(); + this.parent = parent; + this.depends = depends; - this.options = options || {}; + this.options = options || {}; - this.range = null; // Range or Object {start: number, end: number} - this.itemsData = null; // DataSet with items - this.groupsData = null; // DataSet with groups + this.range = null; // Range or Object {start: number, end: number} + this.itemsData = null; // DataSet with items + this.groupsData = null; // DataSet with groups - this.groups = {}; // map with groups + this.groups = {}; // map with groups - this.dom = {}; - this.props = { - labels: { - width: 0 - } - }; + this.dom = {}; + this.props = { + labels: { + width: 0 + } + }; - // TODO: implement right orientation of the labels + // TODO: implement right orientation of the labels - // changes in groups are queued key/value map containing id/action - this.queue = {}; + // changes in groups are queued key/value map containing id/action + this.queue = {}; - var me = this; - this.listeners = { - 'add': function (event, params) { - me._onAdd(params.items); - }, - 'update': function (event, params) { - me._onUpdate(params.items); - }, - 'remove': function (event, params) { - me._onRemove(params.items); - } - }; + var me = this; + this.listeners = { + 'add': function (event, params) { + me._onAdd(params.items); + }, + 'update': function (event, params) { + me._onUpdate(params.items); + }, + 'remove': function (event, params) { + me._onRemove(params.items); + } + }; } GroupSet.prototype = new Panel(); @@ -10551,7 +10013,7 @@ GroupSet.prototype = new Panel(); GroupSet.prototype.setOptions = Component.prototype.setOptions; GroupSet.prototype.setRange = function (range) { - // TODO: implement setRange + // TODO: implement setRange }; /** @@ -10559,14 +10021,14 @@ GroupSet.prototype.setRange = function (range) { * @param {vis.DataSet | null} items */ GroupSet.prototype.setItems = function setItems(items) { - this.itemsData = items; + this.itemsData = items; - for (var id in this.groups) { - if (this.groups.hasOwnProperty(id)) { - var group = this.groups[id]; - group.setItems(items); - } + for (var id in this.groups) { + if (this.groups.hasOwnProperty(id)) { + var group = this.groups[id]; + group.setItems(items); } + } }; /** @@ -10574,7 +10036,7 @@ GroupSet.prototype.setItems = function setItems(items) { * @return {vis.DataSet | null} items */ GroupSet.prototype.getItems = function getItems() { - return this.itemsData; + return this.itemsData; }; /** @@ -10582,7 +10044,7 @@ GroupSet.prototype.getItems = function getItems() { * @param {Range | Object} range A Range or an object containing start and end. */ GroupSet.prototype.setRange = function setRange(range) { - this.range = range; + this.range = range; }; /** @@ -10590,48 +10052,48 @@ GroupSet.prototype.setRange = function setRange(range) { * @param {vis.DataSet} groups */ GroupSet.prototype.setGroups = function setGroups(groups) { - var me = this, - ids; + var me = this, + ids; - // unsubscribe from current dataset - if (this.groupsData) { - util.forEach(this.listeners, function (callback, event) { - me.groupsData.unsubscribe(event, callback); - }); + // unsubscribe from current dataset + if (this.groupsData) { + util.forEach(this.listeners, function (callback, event) { + me.groupsData.unsubscribe(event, callback); + }); - // remove all drawn groups - ids = this.groupsData.getIds(); - this._onRemove(ids); - } + // remove all drawn groups + ids = this.groupsData.getIds(); + this._onRemove(ids); + } - // replace the dataset - if (!groups) { - this.groupsData = null; - } - else if (groups instanceof DataSet) { - this.groupsData = groups; - } - else { - this.groupsData = new DataSet({ - convert: { - start: 'Date', - end: 'Date' - } - }); - this.groupsData.add(groups); - } + // replace the dataset + if (!groups) { + this.groupsData = null; + } + else if (groups instanceof DataSet) { + this.groupsData = groups; + } + else { + this.groupsData = new DataSet({ + convert: { + start: 'Date', + end: 'Date' + } + }); + this.groupsData.add(groups); + } - if (this.groupsData) { - // subscribe to new dataset - var id = this.id; - util.forEach(this.listeners, function (callback, event) { - me.groupsData.subscribe(event, callback, id); - }); + if (this.groupsData) { + // subscribe to new dataset + var id = this.id; + util.forEach(this.listeners, function (callback, event) { + me.groupsData.subscribe(event, callback, id); + }); - // draw all new groups - ids = this.groupsData.getIds(); - this._onAdd(ids); - } + // draw all new groups + ids = this.groupsData.getIds(); + this._onAdd(ids); + } }; /** @@ -10639,7 +10101,7 @@ GroupSet.prototype.setGroups = function setGroups(groups) { * @return {vis.DataSet | null} groups */ GroupSet.prototype.getGroups = function getGroups() { - return this.groupsData; + return this.groupsData; }; /** @@ -10647,167 +10109,179 @@ GroupSet.prototype.getGroups = function getGroups() { * @return {Boolean} changed */ GroupSet.prototype.repaint = function repaint() { - var changed = 0, - i, id, group, label, - update = util.updateProperty, - asSize = util.option.asSize, - asElement = util.option.asElement, - options = this.options, - frame = this.dom.frame, - labels = this.dom.labels; - - // create frame - if (!this.parent) { - throw new Error('Cannot repaint groupset: no parent attached'); - } - var parentContainer = this.parent.getContainer(); - if (!parentContainer) { - throw new Error('Cannot repaint groupset: parent has no container element'); + var changed = 0, + i, id, group, label, + update = util.updateProperty, + asSize = util.option.asSize, + asElement = util.option.asElement, + options = this.options, + frame = this.dom.frame, + labels = this.dom.labels, + labelSet = this.dom.labelSet; + + // create frame + if (!this.parent) { + throw new Error('Cannot repaint groupset: no parent attached'); + } + var parentContainer = this.parent.getContainer(); + if (!parentContainer) { + throw new Error('Cannot repaint groupset: parent has no container element'); + } + if (!frame) { + frame = document.createElement('div'); + frame.className = 'groupset'; + this.dom.frame = frame; + + var className = options.className; + if (className) { + util.addClassName(frame, util.option.asString(className)); } - if (!frame) { - frame = document.createElement('div'); - frame.className = 'groupset'; - this.dom.frame = frame; - var className = options.className; - if (className) { - util.addClassName(frame, util.option.asString(className)); - } + changed += 1; + } + if (!frame.parentNode) { + parentContainer.appendChild(frame); + changed += 1; + } - changed += 1; - } - if (!frame.parentNode) { - parentContainer.appendChild(frame); - changed += 1; + // create labels + var labelContainer = asElement(options.labelContainer); + if (!labelContainer) { + throw new Error('Cannot repaint groupset: option "labelContainer" not defined'); + } + if (!labels) { + labels = document.createElement('div'); + labels.className = 'labels'; + this.dom.labels = labels; + } + if (!labelSet) { + labelSet = document.createElement('div'); + labelSet.className = 'label-set'; + labels.appendChild(labelSet); + this.dom.labelSet = labelSet; + } + if (!labels.parentNode || labels.parentNode != labelContainer) { + if (labels.parentNode) { + labels.parentNode.removeChild(labels.parentNode); } + labelContainer.appendChild(labels); + } - // create labels - var labelContainer = asElement(options.labelContainer); - if (!labelContainer) { - throw new Error('Cannot repaint groupset: option "labelContainer" not defined'); - } - if (!labels) { - labels = document.createElement('div'); - labels.className = 'labels'; - //frame.appendChild(labels); - this.dom.labels = labels; - } - if (!labels.parentNode || labels.parentNode != labelContainer) { - if (labels.parentNode) { - labels.parentNode.removeChild(labels.parentNode); - } - labelContainer.appendChild(labels); - } + // reposition frame + changed += update(frame.style, 'height', asSize(options.height, this.height + 'px')); + changed += update(frame.style, 'top', asSize(options.top, '0px')); + changed += update(frame.style, 'left', asSize(options.left, '0px')); + changed += update(frame.style, 'width', asSize(options.width, '100%')); - // reposition frame - changed += update(frame.style, 'height', asSize(options.height, this.height + 'px')); - changed += update(frame.style, 'top', asSize(options.top, '0px')); - changed += update(frame.style, 'left', asSize(options.left, '0px')); - changed += update(frame.style, 'width', asSize(options.width, '100%')); - - // reposition labels - changed += update(labels.style, 'top', asSize(options.top, '0px')); - - var me = this, - queue = this.queue, - groups = this.groups, - groupsData = this.groupsData; - - // show/hide added/changed/removed groups - var ids = Object.keys(queue); - if (ids.length) { - ids.forEach(function (id) { - var action = queue[id]; - var group = groups[id]; - - //noinspection FallthroughInSwitchStatementJS - switch (action) { - case 'add': - case 'update': - if (!group) { - var groupOptions = Object.create(me.options); - group = new Group(me, id, groupOptions); - group.setItems(me.itemsData); // attach items data - groups[id] = group; - - me.controller.add(group); - } + // reposition labels + changed += update(labelSet.style, 'top', asSize(options.top, '0px')); + changed += update(labelSet.style, 'height', asSize(options.height, this.height + 'px')); - // TODO: update group data - group.data = groupsData.get(id); + var me = this, + queue = this.queue, + groups = this.groups, + groupsData = this.groupsData; - delete queue[id]; - break; + // show/hide added/changed/removed groups + var ids = Object.keys(queue); + if (ids.length) { + ids.forEach(function (id) { + var action = queue[id]; + var group = groups[id]; + + //noinspection FallthroughInSwitchStatementJS + switch (action) { + case 'add': + case 'update': + if (!group) { + var groupOptions = Object.create(me.options); + util.extend(groupOptions, { + height: null, + maxHeight: null + }); - case 'remove': - if (group) { - group.setItems(); // detach items data - delete groups[id]; + group = new Group(me, id, groupOptions); + group.setItems(me.itemsData); // attach items data + groups[id] = group; - me.controller.remove(group); - } + me.controller.add(group); + } - // update lists - delete queue[id]; - break; + // TODO: update group data + group.data = groupsData.get(id); - default: - console.log('Error: unknown action "' + action + '"'); - } - }); + delete queue[id]; + break; - // the groupset depends on each of the groups - //this.depends = this.groups; // TODO: gives a circular reference through the parent + case 'remove': + if (group) { + group.setItems(); // detach items data + delete groups[id]; - // TODO: apply dependencies of the groupset + me.controller.remove(group); + } - // update the top positions of the groups in the correct order - var orderedGroups = this.groupsData.getIds({ - order: this.options.groupsOrder - }); - for (i = 0; i < orderedGroups.length; i++) { - (function (group, prevGroup) { - var top = 0; - if (prevGroup) { - top = function () { - // TODO: top must reckon with options.maxHeight - return prevGroup.top + prevGroup.height; - } - } - group.setOptions({ - top: top - }); - })(groups[orderedGroups[i]], groups[orderedGroups[i - 1]]); - } + // update lists + delete queue[id]; + break; - // (re)create the labels - while (labels.firstChild) { - labels.removeChild(labels.firstChild); - } - for (i = 0; i < orderedGroups.length; i++) { - id = orderedGroups[i]; - label = this._createLabel(id); - labels.appendChild(label); - } + default: + console.log('Error: unknown action "' + action + '"'); + } + }); + + // the groupset depends on each of the groups + //this.depends = this.groups; // TODO: gives a circular reference through the parent + + // TODO: apply dependencies of the groupset + + // update the top positions of the groups in the correct order + var orderedGroups = this.groupsData.getIds({ + order: this.options.groupOrder + }); + for (i = 0; i < orderedGroups.length; i++) { + (function (group, prevGroup) { + var top = 0; + if (prevGroup) { + top = function () { + // TODO: top must reckon with options.maxHeight + return prevGroup.top + prevGroup.height; + } + } + group.setOptions({ + top: top + }); + })(groups[orderedGroups[i]], groups[orderedGroups[i - 1]]); + } - changed++; + // (re)create the labels + while (labelSet.firstChild) { + labelSet.removeChild(labelSet.firstChild); + } + for (i = 0; i < orderedGroups.length; i++) { + id = orderedGroups[i]; + label = this._createLabel(id); + labelSet.appendChild(label); } - // reposition the labels - // TODO: labels are not displayed correctly when orientation=='top' - // TODO: width of labelPanel is not immediately updated on a change in groups - for (id in groups) { - if (groups.hasOwnProperty(id)) { - group = groups[id]; - label = group.label; - if (label) { - label.style.top = group.top + 'px'; - label.style.height = group.height + 'px'; - } - } + changed++; + } + + // reposition the labels + // TODO: labels are not displayed correctly when orientation=='top' + // TODO: width of labelPanel is not immediately updated on a change in groups + for (id in groups) { + if (groups.hasOwnProperty(id)) { + group = groups[id]; + label = group.label; + if (label) { + label.style.top = group.top + 'px'; + label.style.height = group.height + 'px'; + } } + } - return (changed > 0); + return (changed > 0); }; /** @@ -10817,29 +10291,29 @@ GroupSet.prototype.repaint = function repaint() { * @private */ GroupSet.prototype._createLabel = function(id) { - var group = this.groups[id]; - var label = document.createElement('div'); - label.className = 'label'; - var inner = document.createElement('div'); - inner.className = 'inner'; - label.appendChild(inner); + var group = this.groups[id]; + var label = document.createElement('div'); + label.className = 'label'; + var inner = document.createElement('div'); + inner.className = 'inner'; + label.appendChild(inner); + + var content = group.data && group.data.content; + if (content instanceof Element) { + inner.appendChild(content); + } + else if (content != undefined) { + inner.innerHTML = content; + } - var content = group.data && group.data.content; - if (content instanceof Element) { - inner.appendChild(content); - } - else if (content != undefined) { - inner.innerHTML = content; - } + var className = group.data && group.data.className; + if (className) { + util.addClassName(label, className); + } - var className = group.data && group.data.className; - if (className) { - util.addClassName(label, className); - } + group.label = label; // TODO: not so nice, parking labels in the group this way!!! - group.label = label; // TODO: not so nice, parking labels in the group this way!!! - - return label; + return label; }; /** @@ -10847,7 +10321,7 @@ GroupSet.prototype._createLabel = function(id) { * @return {HTMLElement} container */ GroupSet.prototype.getContainer = function getContainer() { - return this.dom.frame; + return this.dom.frame; }; /** @@ -10855,7 +10329,7 @@ GroupSet.prototype.getContainer = function getContainer() { * @return {Number} width */ GroupSet.prototype.getLabelsWidth = function getContainer() { - return this.props.labels.width; + return this.props.labels.width; }; /** @@ -10863,54 +10337,54 @@ GroupSet.prototype.getLabelsWidth = function getContainer() { * @return {Boolean} resized */ GroupSet.prototype.reflow = function reflow() { - var changed = 0, - id, group, - options = this.options, - update = util.updateProperty, - asNumber = util.option.asNumber, - asSize = util.option.asSize, - frame = this.dom.frame; - - if (frame) { - var maxHeight = asNumber(options.maxHeight); - var fixedHeight = (asSize(options.height) != null); - var height; - if (fixedHeight) { - height = frame.offsetHeight; - } - else { - // height is not specified, calculate the sum of the height of all groups - height = 0; - - for (id in this.groups) { - if (this.groups.hasOwnProperty(id)) { - group = this.groups[id]; - height += group.height; - } - } - } - if (maxHeight != null) { - height = Math.min(height, maxHeight); - } - changed += update(this, 'height', height); - - changed += update(this, 'top', frame.offsetTop); - changed += update(this, 'left', frame.offsetLeft); - changed += update(this, 'width', frame.offsetWidth); + var changed = 0, + id, group, + options = this.options, + update = util.updateProperty, + asNumber = util.option.asNumber, + asSize = util.option.asSize, + frame = this.dom.frame; + + if (frame) { + var maxHeight = asNumber(options.maxHeight); + var fixedHeight = (asSize(options.height) != null); + var height; + if (fixedHeight) { + height = frame.offsetHeight; } + else { + // height is not specified, calculate the sum of the height of all groups + height = 0; - // calculate the maximum width of the labels - var width = 0; - for (id in this.groups) { + for (id in this.groups) { if (this.groups.hasOwnProperty(id)) { - group = this.groups[id]; - var labelWidth = group.props && group.props.label && group.props.label.width || 0; - width = Math.max(width, labelWidth); + group = this.groups[id]; + height += group.height; } + } } - changed += update(this.props.labels, 'width', width); + if (maxHeight != null) { + height = Math.min(height, maxHeight); + } + changed += update(this, 'height', height); + + changed += update(this, 'top', frame.offsetTop); + changed += update(this, 'left', frame.offsetLeft); + changed += update(this, 'width', frame.offsetWidth); + } - return (changed > 0); + // calculate the maximum width of the labels + var width = 0; + for (id in this.groups) { + if (this.groups.hasOwnProperty(id)) { + group = this.groups[id]; + var labelWidth = group.props && group.props.label && group.props.label.width || 0; + width = Math.max(width, labelWidth); + } + } + changed += update(this.props.labels, 'width', width); + + return (changed > 0); }; /** @@ -10918,13 +10392,13 @@ GroupSet.prototype.reflow = function reflow() { * @return {Boolean} changed */ GroupSet.prototype.hide = function hide() { - if (this.dom.frame && this.dom.frame.parentNode) { - this.dom.frame.parentNode.removeChild(this.dom.frame); - return true; - } - else { - return false; - } + if (this.dom.frame && this.dom.frame.parentNode) { + this.dom.frame.parentNode.removeChild(this.dom.frame); + return true; + } + else { + return false; + } }; /** @@ -10933,12 +10407,12 @@ GroupSet.prototype.hide = function hide() { * @return {Boolean} changed */ GroupSet.prototype.show = function show() { - if (!this.dom.frame || !this.dom.frame.parentNode) { - return this.repaint(); - } - else { - return false; - } + if (!this.dom.frame || !this.dom.frame.parentNode) { + return this.repaint(); + } + else { + return false; + } }; /** @@ -10947,7 +10421,7 @@ GroupSet.prototype.show = function show() { * @private */ GroupSet.prototype._onUpdate = function _onUpdate(ids) { - this._toQueue(ids, 'update'); + this._toQueue(ids, 'update'); }; /** @@ -10956,7 +10430,7 @@ GroupSet.prototype._onUpdate = function _onUpdate(ids) { * @private */ GroupSet.prototype._onAdd = function _onAdd(ids) { - this._toQueue(ids, 'add'); + this._toQueue(ids, 'add'); }; /** @@ -10965,7 +10439,7 @@ GroupSet.prototype._onAdd = function _onAdd(ids) { * @private */ GroupSet.prototype._onRemove = function _onRemove(ids) { - this._toQueue(ids, 'remove'); + this._toQueue(ids, 'remove'); }; /** @@ -10974,15 +10448,15 @@ GroupSet.prototype._onRemove = function _onRemove(ids) { * @param {String} action can be 'add', 'update', 'remove' */ GroupSet.prototype._toQueue = function _toQueue(ids, action) { - var queue = this.queue; - ids.forEach(function (id) { - queue[id] = action; - }); - - if (this.controller) { - //this.requestReflow(); - this.requestRepaint(); - } + var queue = this.queue; + ids.forEach(function (id) { + queue[id] = action; + }); + + if (this.controller) { + //this.requestReflow(); + this.requestRepaint(); + } }; /** @@ -10993,129 +10467,130 @@ GroupSet.prototype._toQueue = function _toQueue(ids, action) { * @constructor */ function Timeline (container, items, options) { - var me = this; - var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0); - this.options = { - orientation: 'bottom', - min: null, - max: null, - zoomMin: 10, // milliseconds - zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds - // moveable: true, // TODO: option moveable - // zoomable: true, // TODO: option zoomable - showMinorLabels: true, - showMajorLabels: true, - showCurrentTime: false, - showCustomTime: false, - autoResize: false - }; - - // controller - this.controller = new Controller(); - - // root panel - if (!container) { - throw new Error('No container element provided'); + var me = this; + var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0); + this.options = { + orientation: 'bottom', + min: null, + max: null, + zoomMin: 10, // milliseconds + zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds + // moveable: true, // TODO: option moveable + // zoomable: true, // TODO: option zoomable + showMinorLabels: true, + showMajorLabels: true, + showCurrentTime: false, + showCustomTime: false, + autoResize: false + }; + + // controller + this.controller = new Controller(); + + // root panel + if (!container) { + throw new Error('No container element provided'); + } + var rootOptions = Object.create(this.options); + rootOptions.height = function () { + // TODO: change to height + if (me.options.height) { + // fixed height + return me.options.height; } - var rootOptions = Object.create(this.options); - rootOptions.height = function () { - if (me.options.height) { - // fixed height - return me.options.height; - } - else { - // auto height - return me.timeaxis.height + me.content.height; - } - }; - this.rootPanel = new RootPanel(container, rootOptions); - this.controller.add(this.rootPanel); - - // item panel - var itemOptions = Object.create(this.options); - itemOptions.left = function () { - return me.labelPanel.width; - }; - itemOptions.width = function () { - return me.rootPanel.width - me.labelPanel.width; - }; - itemOptions.top = null; - itemOptions.height = null; - this.itemPanel = new Panel(this.rootPanel, [], itemOptions); - this.controller.add(this.itemPanel); - - // label panel - var labelOptions = Object.create(this.options); - labelOptions.top = null; - labelOptions.left = null; - labelOptions.height = null; - labelOptions.width = function () { - if (me.content && typeof me.content.getLabelsWidth === 'function') { - return me.content.getLabelsWidth(); - } - else { - return 0; - } - }; - this.labelPanel = new Panel(this.rootPanel, [], labelOptions); - this.controller.add(this.labelPanel); - - // range - var rangeOptions = Object.create(this.options); - this.range = new Range(rangeOptions); - this.range.setRange( - now.clone().add('days', -3).valueOf(), - now.clone().add('days', 4).valueOf() - ); - - // TODO: reckon with options moveable and zoomable - this.range.subscribe(this.rootPanel, 'move', 'horizontal'); - this.range.subscribe(this.rootPanel, 'zoom', 'horizontal'); - this.range.on('rangechange', function () { - var force = true; - me.controller.requestReflow(force); - }); - this.range.on('rangechanged', function () { - var force = true; - me.controller.requestReflow(force); - }); - - // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable - - // time axis - var timeaxisOptions = Object.create(rootOptions); - timeaxisOptions.range = this.range; - timeaxisOptions.left = null; - timeaxisOptions.top = null; - timeaxisOptions.width = '100%'; - timeaxisOptions.height = null; - this.timeaxis = new TimeAxis(this.itemPanel, [], timeaxisOptions); - this.timeaxis.setRange(this.range); - this.controller.add(this.timeaxis); - - // current time bar - this.currenttime = new CurrentTime(this.timeaxis, [], rootOptions); - this.controller.add(this.currenttime); - - // custom time bar - this.customtime = new CustomTime(this.timeaxis, [], rootOptions); - this.controller.add(this.customtime); - - // create itemset or groupset - this.setGroups(null); - - this.itemsData = null; // DataSet - this.groupsData = null; // DataSet - - // apply options - if (options) { - this.setOptions(options); + else { + // auto height + return (me.timeaxis.height + me.content.height) + 'px'; + } + }; + this.rootPanel = new RootPanel(container, rootOptions); + this.controller.add(this.rootPanel); + + // item panel + var itemOptions = Object.create(this.options); + itemOptions.left = function () { + return me.labelPanel.width; + }; + itemOptions.width = function () { + return me.rootPanel.width - me.labelPanel.width; + }; + itemOptions.top = null; + itemOptions.height = null; + this.itemPanel = new Panel(this.rootPanel, [], itemOptions); + this.controller.add(this.itemPanel); + + // label panel + var labelOptions = Object.create(this.options); + labelOptions.top = null; + labelOptions.left = null; + labelOptions.height = null; + labelOptions.width = function () { + if (me.content && typeof me.content.getLabelsWidth === 'function') { + return me.content.getLabelsWidth(); } + else { + return 0; + } + }; + this.labelPanel = new Panel(this.rootPanel, [], labelOptions); + this.controller.add(this.labelPanel); + + // range + var rangeOptions = Object.create(this.options); + this.range = new Range(rangeOptions); + this.range.setRange( + now.clone().add('days', -3).valueOf(), + now.clone().add('days', 4).valueOf() + ); + + // TODO: reckon with options moveable and zoomable + this.range.subscribe(this.rootPanel, 'move', 'horizontal'); + this.range.subscribe(this.rootPanel, 'zoom', 'horizontal'); + this.range.on('rangechange', function () { + var force = true; + me.controller.requestReflow(force); + }); + this.range.on('rangechanged', function () { + var force = true; + me.controller.requestReflow(force); + }); + + // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable + + // time axis + var timeaxisOptions = Object.create(rootOptions); + timeaxisOptions.range = this.range; + timeaxisOptions.left = null; + timeaxisOptions.top = null; + timeaxisOptions.width = '100%'; + timeaxisOptions.height = null; + this.timeaxis = new TimeAxis(this.itemPanel, [], timeaxisOptions); + this.timeaxis.setRange(this.range); + this.controller.add(this.timeaxis); + + // current time bar + this.currenttime = new CurrentTime(this.timeaxis, [], rootOptions); + this.controller.add(this.currenttime); + + // custom time bar + this.customtime = new CustomTime(this.timeaxis, [], rootOptions); + this.controller.add(this.customtime); + + // create groupset + this.setGroups(null); + + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet + + // apply options + if (options) { + this.setOptions(options); + } - // set data (must be after options are applied) - if (items) { - this.setItems(items); - } + // create itemset and groupset + if (items) { + this.setItems(items); + } } /** @@ -11123,15 +10598,15 @@ function Timeline (container, items, options) { * @param {Object} options TODO: describe the available options */ Timeline.prototype.setOptions = function (options) { - util.extend(this.options, options); + util.extend(this.options, options); - // force update of range - // options.start and options.end can be undefined - //this.range.setRange(options.start, options.end); - this.range.setRange(); + // force update of range + // options.start and options.end can be undefined + //this.range.setRange(options.start, options.end); + this.range.setRange(); - this.controller.reflow(); - this.controller.repaint(); + this.controller.reflow(); + this.controller.repaint(); }; /** @@ -11139,7 +10614,7 @@ Timeline.prototype.setOptions = function (options) { * @param {Date} time */ Timeline.prototype.setCustomTime = function (time) { - this.customtime._setCustomTime(time); + this.customtime._setCustomTime(time); }; /** @@ -11147,7 +10622,7 @@ Timeline.prototype.setCustomTime = function (time) { * @return {Date} customTime */ Timeline.prototype.getCustomTime = function() { - return new Date(this.customtime.customTime.valueOf()); + return new Date(this.customtime.customTime.valueOf()); }; /** @@ -11155,60 +10630,60 @@ Timeline.prototype.getCustomTime = function() { * @param {vis.DataSet | Array | DataTable | null} items */ Timeline.prototype.setItems = function(items) { - var initialLoad = (this.itemsData == null); + var initialLoad = (this.itemsData == null); - // convert to type DataSet when needed - var newItemSet; - if (!items) { - newItemSet = null; + // convert to type DataSet when needed + var newItemSet; + if (!items) { + newItemSet = null; + } + else if (items instanceof DataSet) { + newItemSet = items; + } + if (!(items instanceof DataSet)) { + newItemSet = new DataSet({ + convert: { + start: 'Date', + end: 'Date' + } + }); + newItemSet.add(items); + } + + // set items + this.itemsData = newItemSet; + this.content.setItems(newItemSet); + + if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) { + // apply the data range as range + var dataRange = this.getItemRange(); + + // add 5% space on both sides + var min = dataRange.min; + var max = dataRange.max; + if (min != null && max != null) { + var interval = (max.valueOf() - min.valueOf()); + if (interval <= 0) { + // prevent an empty interval + interval = 24 * 60 * 60 * 1000; // 1 day + } + min = new Date(min.valueOf() - interval * 0.05); + max = new Date(max.valueOf() + interval * 0.05); } - else if (items instanceof DataSet) { - newItemSet = items; + + // override specified start and/or end date + if (this.options.start != undefined) { + min = util.convert(this.options.start, 'Date'); } - if (!(items instanceof DataSet)) { - newItemSet = new DataSet({ - convert: { - start: 'Date', - end: 'Date' - } - }); - newItemSet.add(items); + if (this.options.end != undefined) { + max = util.convert(this.options.end, 'Date'); } - // set items - this.itemsData = newItemSet; - this.content.setItems(newItemSet); - - if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) { - // apply the data range as range - var dataRange = this.getItemRange(); - - // add 5% space on both sides - var min = dataRange.min; - var max = dataRange.max; - if (min != null && max != null) { - var interval = (max.valueOf() - min.valueOf()); - if (interval <= 0) { - // prevent an empty interval - interval = 24 * 60 * 60 * 1000; // 1 day - } - min = new Date(min.valueOf() - interval * 0.05); - max = new Date(max.valueOf() + interval * 0.05); - } - - // override specified start and/or end date - if (this.options.start != undefined) { - min = util.convert(this.options.start, 'Date'); - } - if (this.options.end != undefined) { - max = util.convert(this.options.end, 'Date'); - } - - // apply range if there is a min or max available - if (min != null || max != null) { - this.range.setRange(min, max); - } + // apply range if there is a min or max available + if (min != null || max != null) { + this.range.setRange(min, max); } + } }; /** @@ -11216,72 +10691,76 @@ Timeline.prototype.setItems = function(items) { * @param {vis.DataSet | Array | DataTable} groups */ Timeline.prototype.setGroups = function(groups) { - var me = this; - this.groupsData = groups; + var me = this; + this.groupsData = groups; + + // switch content type between ItemSet or GroupSet when needed + var Type = this.groupsData ? GroupSet : ItemSet; + if (!(this.content instanceof Type)) { + // remove old content set + if (this.content) { + this.content.hide(); + if (this.content.setItems) { + this.content.setItems(); // disconnect from items + } + if (this.content.setGroups) { + this.content.setGroups(); // disconnect from groups + } + this.controller.remove(this.content); + } - // switch content type between ItemSet or GroupSet when needed - var type = this.groupsData ? GroupSet : ItemSet; - if (!(this.content instanceof type)) { - // remove old content set - if (this.content) { - this.content.hide(); - if (this.content.setItems) { - this.content.setItems(); // disconnect from items - } - if (this.content.setGroups) { - this.content.setGroups(); // disconnect from groups - } - this.controller.remove(this.content); + // create new content set + var options = Object.create(this.options); + util.extend(options, { + top: function () { + if (me.options.orientation == 'top') { + return me.timeaxis.height; } - - // create new content set - var options = Object.create(this.options); - util.extend(options, { - top: function () { - if (me.options.orientation == 'top') { - return me.timeaxis.height; - } - else { - return me.itemPanel.height - me.timeaxis.height - me.content.height; - } - }, - left: null, - width: '100%', - height: function () { - if (me.options.height) { - return me.itemPanel.height - me.timeaxis.height; - } - else { - return null; - } - }, - maxHeight: function () { - if (me.options.maxHeight) { - if (!util.isNumber(me.options.maxHeight)) { - throw new TypeError('Number expected for property maxHeight'); - } - return me.options.maxHeight - me.timeaxis.height; - } - else { - return null; - } - }, - labelContainer: function () { - return me.labelPanel.getContainer(); - } - }); - this.content = new type(this.itemPanel, [this.timeaxis], options); - if (this.content.setRange) { - this.content.setRange(this.range); + else { + return me.itemPanel.height - me.timeaxis.height - me.content.height; } - if (this.content.setItems) { - this.content.setItems(this.itemsData); + }, + left: null, + width: '100%', + height: function () { + if (me.options.height) { + // fixed height + return me.itemPanel.height - me.timeaxis.height; } - if (this.content.setGroups) { - this.content.setGroups(this.groupsData); + else { + // auto height + return null; + } + }, + maxHeight: function () { + // TODO: change maxHeight to be a css string like '100%' or '300px' + if (me.options.maxHeight) { + if (!util.isNumber(me.options.maxHeight)) { + throw new TypeError('Number expected for property maxHeight'); + } + return me.options.maxHeight - me.timeaxis.height; } - this.controller.add(this.content); + else { + return null; + } + }, + labelContainer: function () { + return me.labelPanel.getContainer(); + } + }); + + this.content = new Type(this.itemPanel, [this.timeaxis], options); + if (this.content.setRange) { + this.content.setRange(this.range); + } + if (this.content.setItems) { + this.content.setItems(this.itemsData); } + if (this.content.setGroups) { + this.content.setGroups(this.groupsData); + } + this.controller.add(this.content); + } }; /** @@ -11291,1092 +10770,1092 @@ Timeline.prototype.setGroups = function(groups) { * When no maximum is found, max==null */ Timeline.prototype.getItemRange = function getItemRange() { - // calculate min from start filed - var itemsData = this.itemsData, - min = null, - max = null; - - if (itemsData) { - // calculate the minimum value of the field 'start' - var minItem = itemsData.min('start'); - min = minItem ? minItem.start.valueOf() : null; - - // calculate maximum value of fields 'start' and 'end' - var maxStartItem = itemsData.max('start'); - if (maxStartItem) { - max = maxStartItem.start.valueOf(); - } - var maxEndItem = itemsData.max('end'); - if (maxEndItem) { - if (max == null) { - max = maxEndItem.end.valueOf(); - } - else { - max = Math.max(max, maxEndItem.end.valueOf()); - } - } + // calculate min from start filed + var itemsData = this.itemsData, + min = null, + max = null; + + if (itemsData) { + // calculate the minimum value of the field 'start' + var minItem = itemsData.min('start'); + min = minItem ? minItem.start.valueOf() : null; + + // calculate maximum value of fields 'start' and 'end' + var maxStartItem = itemsData.max('start'); + if (maxStartItem) { + max = maxStartItem.start.valueOf(); + } + var maxEndItem = itemsData.max('end'); + if (maxEndItem) { + if (max == null) { + max = maxEndItem.end.valueOf(); + } + else { + max = Math.max(max, maxEndItem.end.valueOf()); + } } + } - return { - min: (min != null) ? new Date(min) : null, - max: (max != null) ? new Date(max) : null - }; + return { + min: (min != null) ? new Date(min) : null, + max: (max != null) ? new Date(max) : null + }; }; (function(exports) { - /** - * Parse a text source containing data in DOT language into a JSON object. - * The object contains two lists: one with nodes and one with edges. - * - * DOT language reference: http://www.graphviz.org/doc/info/lang.html - * - * @param {String} data Text containing a graph in DOT-notation - * @return {Object} graph An object containing two parameters: - * {Object[]} nodes - * {Object[]} edges - */ - function parseDOT (data) { - dot = data; - return parseGraph(); - } + /** + * Parse a text source containing data in DOT language into a JSON object. + * The object contains two lists: one with nodes and one with edges. + * + * DOT language reference: http://www.graphviz.org/doc/info/lang.html + * + * @param {String} data Text containing a graph in DOT-notation + * @return {Object} graph An object containing two parameters: + * {Object[]} nodes + * {Object[]} edges + */ + function parseDOT (data) { + dot = data; + return parseGraph(); + } - // token types enumeration - var TOKENTYPE = { - NULL : 0, - DELIMITER : 1, - IDENTIFIER: 2, - UNKNOWN : 3 - }; + // token types enumeration + var TOKENTYPE = { + NULL : 0, + DELIMITER : 1, + IDENTIFIER: 2, + UNKNOWN : 3 + }; + + // map with all delimiters + var DELIMITERS = { + '{': true, + '}': true, + '[': true, + ']': true, + ';': true, + '=': true, + ',': true, + + '->': true, + '--': true + }; + + var dot = ''; // current dot file + var index = 0; // current index in dot file + var c = ''; // current token character in expr + var token = ''; // current token + var tokenType = TOKENTYPE.NULL; // type of the token + + /** + * Get the first character from the dot file. + * The character is stored into the char c. If the end of the dot file is + * reached, the function puts an empty string in c. + */ + function first() { + index = 0; + c = dot.charAt(0); + } - // map with all delimiters - var DELIMITERS = { - '{': true, - '}': true, - '[': true, - ']': true, - ';': true, - '=': true, - ',': true, - - '->': true, - '--': true - }; + /** + * Get the next character from the dot file. + * The character is stored into the char c. If the end of the dot file is + * reached, the function puts an empty string in c. + */ + function next() { + index++; + c = dot.charAt(index); + } - var dot = ''; // current dot file - var index = 0; // current index in dot file - var c = ''; // current token character in expr - var token = ''; // current token - var tokenType = TOKENTYPE.NULL; // type of the token + /** + * Preview the next character from the dot file. + * @return {String} cNext + */ + function nextPreview() { + return dot.charAt(index + 1); + } - /** - * Get the first character from the dot file. - * The character is stored into the char c. If the end of the dot file is - * reached, the function puts an empty string in c. - */ - function first() { - index = 0; - c = dot.charAt(0); - } + /** + * Test whether given character is alphabetic or numeric + * @param {String} c + * @return {Boolean} isAlphaNumeric + */ + var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/; + function isAlphaNumeric(c) { + return regexAlphaNumeric.test(c); + } - /** - * Get the next character from the dot file. - * The character is stored into the char c. If the end of the dot file is - * reached, the function puts an empty string in c. - */ - function next() { - index++; - c = dot.charAt(index); + /** + * Merge all properties of object b into object b + * @param {Object} a + * @param {Object} b + * @return {Object} a + */ + function merge (a, b) { + if (!a) { + a = {}; } - /** - * Preview the next character from the dot file. - * @return {String} cNext - */ - function nextPreview() { - return dot.charAt(index + 1); + if (b) { + for (var name in b) { + if (b.hasOwnProperty(name)) { + a[name] = b[name]; + } + } } + return a; + } - /** - * Test whether given character is alphabetic or numeric - * @param {String} c - * @return {Boolean} isAlphaNumeric - */ - var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/; - function isAlphaNumeric(c) { - return regexAlphaNumeric.test(c); + /** + * Set a value in an object, where the provided parameter name can be a + * path with nested parameters. For example: + * + * var obj = {a: 2}; + * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}} + * + * @param {Object} obj + * @param {String} path A parameter name or dot-separated parameter path, + * like "color.highlight.border". + * @param {*} value + */ + function setValue(obj, path, value) { + var keys = path.split('.'); + var o = obj; + while (keys.length) { + var key = keys.shift(); + if (keys.length) { + // this isn't the end point + if (!o[key]) { + o[key] = {}; + } + o = o[key]; + } + else { + // this is the end point + o[key] = value; + } } + } - /** - * Merge all properties of object b into object b - * @param {Object} a - * @param {Object} b - * @return {Object} a - */ - function merge (a, b) { - if (!a) { - a = {}; - } - - if (b) { - for (var name in b) { - if (b.hasOwnProperty(name)) { - a[name] = b[name]; - } - } + /** + * Add a node to a graph object. If there is already a node with + * the same id, their attributes will be merged. + * @param {Object} graph + * @param {Object} node + */ + function addNode(graph, node) { + var i, len; + var current = null; + + // find root graph (in case of subgraph) + var graphs = [graph]; // list with all graphs from current graph to root graph + var root = graph; + while (root.parent) { + graphs.push(root.parent); + root = root.parent; + } + + // find existing node (at root level) by its id + if (root.nodes) { + for (i = 0, len = root.nodes.length; i < len; i++) { + if (node.id === root.nodes[i].id) { + current = root.nodes[i]; + break; } - return a; + } } - /** - * Set a value in an object, where the provided parameter name can be a - * path with nested parameters. For example: - * - * var obj = {a: 2}; - * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}} - * - * @param {Object} obj - * @param {String} path A parameter name or dot-separated parameter path, - * like "color.highlight.border". - * @param {*} value - */ - function setValue(obj, path, value) { - var keys = path.split('.'); - var o = obj; - while (keys.length) { - var key = keys.shift(); - if (keys.length) { - // this isn't the end point - if (!o[key]) { - o[key] = {}; - } - o = o[key]; - } - else { - // this is the end point - o[key] = value; - } - } + if (!current) { + // this is a new node + current = { + id: node.id + }; + if (graph.node) { + // clone default attributes + current.attr = merge(current.attr, graph.node); + } } - /** - * Add a node to a graph object. If there is already a node with - * the same id, their attributes will be merged. - * @param {Object} graph - * @param {Object} node - */ - function addNode(graph, node) { - var i, len; - var current = null; - - // find root graph (in case of subgraph) - var graphs = [graph]; // list with all graphs from current graph to root graph - var root = graph; - while (root.parent) { - graphs.push(root.parent); - root = root.parent; - } - - // find existing node (at root level) by its id - if (root.nodes) { - for (i = 0, len = root.nodes.length; i < len; i++) { - if (node.id === root.nodes[i].id) { - current = root.nodes[i]; - break; - } - } - } - - if (!current) { - // this is a new node - current = { - id: node.id - }; - if (graph.node) { - // clone default attributes - current.attr = merge(current.attr, graph.node); - } - } + // add node to this (sub)graph and all its parent graphs + for (i = graphs.length - 1; i >= 0; i--) { + var g = graphs[i]; - // add node to this (sub)graph and all its parent graphs - for (i = graphs.length - 1; i >= 0; i--) { - var g = graphs[i]; - - if (!g.nodes) { - g.nodes = []; - } - if (g.nodes.indexOf(current) == -1) { - g.nodes.push(current); - } - } - - // merge attributes - if (node.attr) { - current.attr = merge(current.attr, node.attr); - } + if (!g.nodes) { + g.nodes = []; + } + if (g.nodes.indexOf(current) == -1) { + g.nodes.push(current); + } } - /** - * Add an edge to a graph object - * @param {Object} graph - * @param {Object} edge - */ - function addEdge(graph, edge) { - if (!graph.edges) { - graph.edges = []; - } - graph.edges.push(edge); - if (graph.edge) { - var attr = merge({}, graph.edge); // clone default attributes - edge.attr = merge(attr, edge.attr); // merge attributes - } + // merge attributes + if (node.attr) { + current.attr = merge(current.attr, node.attr); } + } - /** - * Create an edge to a graph object - * @param {Object} graph - * @param {String | Number | Object} from - * @param {String | Number | Object} to - * @param {String} type - * @param {Object | null} attr - * @return {Object} edge - */ - function createEdge(graph, from, to, type, attr) { - var edge = { - from: from, - to: to, - type: type - }; + /** + * Add an edge to a graph object + * @param {Object} graph + * @param {Object} edge + */ + function addEdge(graph, edge) { + if (!graph.edges) { + graph.edges = []; + } + graph.edges.push(edge); + if (graph.edge) { + var attr = merge({}, graph.edge); // clone default attributes + edge.attr = merge(attr, edge.attr); // merge attributes + } + } - if (graph.edge) { - edge.attr = merge({}, graph.edge); // clone default attributes - } - edge.attr = merge(edge.attr || {}, attr); // merge attributes + /** + * Create an edge to a graph object + * @param {Object} graph + * @param {String | Number | Object} from + * @param {String | Number | Object} to + * @param {String} type + * @param {Object | null} attr + * @return {Object} edge + */ + function createEdge(graph, from, to, type, attr) { + var edge = { + from: from, + to: to, + type: type + }; - return edge; + if (graph.edge) { + edge.attr = merge({}, graph.edge); // clone default attributes } + edge.attr = merge(edge.attr || {}, attr); // merge attributes - /** - * Get next token in the current dot file. - * The token and token type are available as token and tokenType - */ - function getToken() { - tokenType = TOKENTYPE.NULL; - token = ''; - - // skip over whitespaces - while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter - next(); - } + return edge; + } - do { - var isComment = false; + /** + * Get next token in the current dot file. + * The token and token type are available as token and tokenType + */ + function getToken() { + tokenType = TOKENTYPE.NULL; + token = ''; - // skip comment - if (c == '#') { - // find the previous non-space character - var i = index - 1; - while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') { - i--; - } - if (dot.charAt(i) == '\n' || dot.charAt(i) == '') { - // the # is at the start of a line, this is indeed a line comment - while (c != '' && c != '\n') { - next(); - } - isComment = true; - } - } - if (c == '/' && nextPreview() == '/') { - // skip line comment - while (c != '' && c != '\n') { - next(); - } - isComment = true; - } - if (c == '/' && nextPreview() == '*') { - // skip block comment - while (c != '') { - if (c == '*' && nextPreview() == '/') { - // end of block comment found. skip these last two characters - next(); - next(); - break; - } - else { - next(); - } - } - isComment = true; - } + // skip over whitespaces + while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter + next(); + } - // skip over whitespaces - while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter - next(); - } - } - while (isComment); + do { + var isComment = false; - // check for end of dot file - if (c == '') { - // token is still empty - tokenType = TOKENTYPE.DELIMITER; - return; + // skip comment + if (c == '#') { + // find the previous non-space character + var i = index - 1; + while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') { + i--; } - - // check for delimiters consisting of 2 characters - var c2 = c + nextPreview(); - if (DELIMITERS[c2]) { - tokenType = TOKENTYPE.DELIMITER; - token = c2; - next(); + if (dot.charAt(i) == '\n' || dot.charAt(i) == '') { + // the # is at the start of a line, this is indeed a line comment + while (c != '' && c != '\n') { next(); - return; + } + isComment = true; } - - // check for delimiters consisting of 1 character - if (DELIMITERS[c]) { - tokenType = TOKENTYPE.DELIMITER; - token = c; - next(); - return; + } + if (c == '/' && nextPreview() == '/') { + // skip line comment + while (c != '' && c != '\n') { + next(); } - - // check for an identifier (number or string) - // TODO: more precise parsing of numbers/strings (and the port separator ':') - if (isAlphaNumeric(c) || c == '-') { - token += c; + isComment = true; + } + if (c == '/' && nextPreview() == '*') { + // skip block comment + while (c != '') { + if (c == '*' && nextPreview() == '/') { + // end of block comment found. skip these last two characters next(); - - while (isAlphaNumeric(c)) { - token += c; - next(); - } - if (token == 'false') { - token = false; // convert to boolean - } - else if (token == 'true') { - token = true; // convert to boolean - } - else if (!isNaN(Number(token))) { - token = Number(token); // convert to number - } - tokenType = TOKENTYPE.IDENTIFIER; - return; - } - - // check for a string enclosed by double quotes - if (c == '"') { next(); - while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) { - token += c; - if (c == '"') { // skip the escape character - next(); - } - next(); - } - if (c != '"') { - throw newSyntaxError('End of string " expected'); - } + break; + } + else { next(); - tokenType = TOKENTYPE.IDENTIFIER; - return; + } } + isComment = true; + } - // something unknown is found, wrong characters, a syntax error - tokenType = TOKENTYPE.UNKNOWN; - while (c != '') { - token += c; - next(); - } - throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"'); + // skip over whitespaces + while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter + next(); + } } + while (isComment); - /** - * Parse a graph. - * @returns {Object} graph - */ - function parseGraph() { - var graph = {}; - - first(); - getToken(); - - // optional strict keyword - if (token == 'strict') { - graph.strict = true; - getToken(); - } - - // graph or digraph keyword - if (token == 'graph' || token == 'digraph') { - graph.type = token; - getToken(); - } - - // optional graph id - if (tokenType == TOKENTYPE.IDENTIFIER) { - graph.id = token; - getToken(); - } - - // open angle bracket - if (token != '{') { - throw newSyntaxError('Angle bracket { expected'); - } - getToken(); - - // statements - parseStatements(graph); + // check for end of dot file + if (c == '') { + // token is still empty + tokenType = TOKENTYPE.DELIMITER; + return; + } - // close angle bracket - if (token != '}') { - throw newSyntaxError('Angle bracket } expected'); - } - getToken(); + // check for delimiters consisting of 2 characters + var c2 = c + nextPreview(); + if (DELIMITERS[c2]) { + tokenType = TOKENTYPE.DELIMITER; + token = c2; + next(); + next(); + return; + } - // end of file - if (token !== '') { - throw newSyntaxError('End of file expected'); - } - getToken(); + // check for delimiters consisting of 1 character + if (DELIMITERS[c]) { + tokenType = TOKENTYPE.DELIMITER; + token = c; + next(); + return; + } - // remove temporary default properties - delete graph.node; - delete graph.edge; - delete graph.graph; + // check for an identifier (number or string) + // TODO: more precise parsing of numbers/strings (and the port separator ':') + if (isAlphaNumeric(c) || c == '-') { + token += c; + next(); - return graph; + while (isAlphaNumeric(c)) { + token += c; + next(); + } + if (token == 'false') { + token = false; // convert to boolean + } + else if (token == 'true') { + token = true; // convert to boolean + } + else if (!isNaN(Number(token))) { + token = Number(token); // convert to number + } + tokenType = TOKENTYPE.IDENTIFIER; + return; } - /** - * Parse a list with statements. - * @param {Object} graph - */ - function parseStatements (graph) { - while (token !== '' && token != '}') { - parseStatement(graph); - if (token == ';') { - getToken(); - } + // check for a string enclosed by double quotes + if (c == '"') { + next(); + while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) { + token += c; + if (c == '"') { // skip the escape character + next(); } + next(); + } + if (c != '"') { + throw newSyntaxError('End of string " expected'); + } + next(); + tokenType = TOKENTYPE.IDENTIFIER; + return; } - /** - * Parse a single statement. Can be a an attribute statement, node - * statement, a series of node statements and edge statements, or a - * parameter. - * @param {Object} graph - */ - function parseStatement(graph) { - // parse subgraph - var subgraph = parseSubgraph(graph); - if (subgraph) { - // edge statements - parseEdge(graph, subgraph); + // something unknown is found, wrong characters, a syntax error + tokenType = TOKENTYPE.UNKNOWN; + while (c != '') { + token += c; + next(); + } + throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"'); + } - return; - } + /** + * Parse a graph. + * @returns {Object} graph + */ + function parseGraph() { + var graph = {}; - // parse an attribute statement - var attr = parseAttributeStatement(graph); - if (attr) { - return; - } + first(); + getToken(); - // parse node - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError('Identifier expected'); - } - var id = token; // id can be a string or a number - getToken(); + // optional strict keyword + if (token == 'strict') { + graph.strict = true; + getToken(); + } - if (token == '=') { - // id statement - getToken(); - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError('Identifier expected'); - } - graph[id] = token; - getToken(); - // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] " - } - else { - parseNodeStatement(graph, id); - } + // graph or digraph keyword + if (token == 'graph' || token == 'digraph') { + graph.type = token; + getToken(); } - /** - * Parse a subgraph - * @param {Object} graph parent graph object - * @return {Object | null} subgraph - */ - function parseSubgraph (graph) { - var subgraph = null; - - // optional subgraph keyword - if (token == 'subgraph') { - subgraph = {}; - subgraph.type = 'subgraph'; - getToken(); - - // optional graph id - if (tokenType == TOKENTYPE.IDENTIFIER) { - subgraph.id = token; - getToken(); - } - } + // optional graph id + if (tokenType == TOKENTYPE.IDENTIFIER) { + graph.id = token; + getToken(); + } - // open angle bracket - if (token == '{') { - getToken(); + // open angle bracket + if (token != '{') { + throw newSyntaxError('Angle bracket { expected'); + } + getToken(); - if (!subgraph) { - subgraph = {}; - } - subgraph.parent = graph; - subgraph.node = graph.node; - subgraph.edge = graph.edge; - subgraph.graph = graph.graph; + // statements + parseStatements(graph); - // statements - parseStatements(subgraph); + // close angle bracket + if (token != '}') { + throw newSyntaxError('Angle bracket } expected'); + } + getToken(); - // close angle bracket - if (token != '}') { - throw newSyntaxError('Angle bracket } expected'); - } - getToken(); + // end of file + if (token !== '') { + throw newSyntaxError('End of file expected'); + } + getToken(); - // remove temporary default properties - delete subgraph.node; - delete subgraph.edge; - delete subgraph.graph; - delete subgraph.parent; + // remove temporary default properties + delete graph.node; + delete graph.edge; + delete graph.graph; - // register at the parent graph - if (!graph.subgraphs) { - graph.subgraphs = []; - } - graph.subgraphs.push(subgraph); - } + return graph; + } - return subgraph; + /** + * Parse a list with statements. + * @param {Object} graph + */ + function parseStatements (graph) { + while (token !== '' && token != '}') { + parseStatement(graph); + if (token == ';') { + getToken(); + } } + } - /** - * parse an attribute statement like "node [shape=circle fontSize=16]". - * Available keywords are 'node', 'edge', 'graph'. - * The previous list with default attributes will be replaced - * @param {Object} graph - * @returns {String | null} keyword Returns the name of the parsed attribute - * (node, edge, graph), or null if nothing - * is parsed. - */ - function parseAttributeStatement (graph) { - // attribute statements - if (token == 'node') { - getToken(); - - // node attributes - graph.node = parseAttributeList(); - return 'node'; - } - else if (token == 'edge') { - getToken(); + /** + * Parse a single statement. Can be a an attribute statement, node + * statement, a series of node statements and edge statements, or a + * parameter. + * @param {Object} graph + */ + function parseStatement(graph) { + // parse subgraph + var subgraph = parseSubgraph(graph); + if (subgraph) { + // edge statements + parseEdge(graph, subgraph); - // edge attributes - graph.edge = parseAttributeList(); - return 'edge'; - } - else if (token == 'graph') { - getToken(); - - // graph attributes - graph.graph = parseAttributeList(); - return 'graph'; - } + return; + } - return null; + // parse an attribute statement + var attr = parseAttributeStatement(graph); + if (attr) { + return; } - /** - * parse a node statement - * @param {Object} graph - * @param {String | Number} id - */ - function parseNodeStatement(graph, id) { - // node statement - var node = { - id: id - }; - var attr = parseAttributeList(); - if (attr) { - node.attr = attr; - } - addNode(graph, node); + // parse node + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError('Identifier expected'); + } + var id = token; // id can be a string or a number + getToken(); - // edge statements - parseEdge(graph, id); + if (token == '=') { + // id statement + getToken(); + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError('Identifier expected'); + } + graph[id] = token; + getToken(); + // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] " } + else { + parseNodeStatement(graph, id); + } + } - /** - * Parse an edge or a series of edges - * @param {Object} graph - * @param {String | Number} from Id of the from node - */ - function parseEdge(graph, from) { - while (token == '->' || token == '--') { - var to; - var type = token; - getToken(); - - var subgraph = parseSubgraph(graph); - if (subgraph) { - to = subgraph; - } - else { - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError('Identifier or subgraph expected'); - } - to = token; - addNode(graph, { - id: to - }); - getToken(); - } + /** + * Parse a subgraph + * @param {Object} graph parent graph object + * @return {Object | null} subgraph + */ + function parseSubgraph (graph) { + var subgraph = null; + + // optional subgraph keyword + if (token == 'subgraph') { + subgraph = {}; + subgraph.type = 'subgraph'; + getToken(); + + // optional graph id + if (tokenType == TOKENTYPE.IDENTIFIER) { + subgraph.id = token; + getToken(); + } + } - // parse edge attributes - var attr = parseAttributeList(); + // open angle bracket + if (token == '{') { + getToken(); - // create edge - var edge = createEdge(graph, from, to, type, attr); - addEdge(graph, edge); + if (!subgraph) { + subgraph = {}; + } + subgraph.parent = graph; + subgraph.node = graph.node; + subgraph.edge = graph.edge; + subgraph.graph = graph.graph; - from = to; - } - } + // statements + parseStatements(subgraph); - /** - * Parse a set with attributes, - * for example [label="1.000", shape=solid] - * @return {Object | null} attr - */ - function parseAttributeList() { - var attr = null; - - while (token == '[') { - getToken(); - attr = {}; - while (token !== '' && token != ']') { - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError('Attribute name expected'); - } - var name = token; + // close angle bracket + if (token != '}') { + throw newSyntaxError('Angle bracket } expected'); + } + getToken(); - getToken(); - if (token != '=') { - throw newSyntaxError('Equal sign = expected'); - } - getToken(); + // remove temporary default properties + delete subgraph.node; + delete subgraph.edge; + delete subgraph.graph; + delete subgraph.parent; - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError('Attribute value expected'); - } - var value = token; - setValue(attr, name, value); // name can be a path + // register at the parent graph + if (!graph.subgraphs) { + graph.subgraphs = []; + } + graph.subgraphs.push(subgraph); + } - getToken(); - if (token ==',') { - getToken(); - } - } + return subgraph; + } - if (token != ']') { - throw newSyntaxError('Bracket ] expected'); - } - getToken(); - } + /** + * parse an attribute statement like "node [shape=circle fontSize=16]". + * Available keywords are 'node', 'edge', 'graph'. + * The previous list with default attributes will be replaced + * @param {Object} graph + * @returns {String | null} keyword Returns the name of the parsed attribute + * (node, edge, graph), or null if nothing + * is parsed. + */ + function parseAttributeStatement (graph) { + // attribute statements + if (token == 'node') { + getToken(); - return attr; + // node attributes + graph.node = parseAttributeList(); + return 'node'; } + else if (token == 'edge') { + getToken(); - /** - * Create a syntax error with extra information on current token and index. - * @param {String} message - * @returns {SyntaxError} err - */ - function newSyntaxError(message) { - return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')'); + // edge attributes + graph.edge = parseAttributeList(); + return 'edge'; } + else if (token == 'graph') { + getToken(); - /** - * Chop off text after a maximum length - * @param {String} text - * @param {Number} maxLength - * @returns {String} - */ - function chop (text, maxLength) { - return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...'); + // graph attributes + graph.graph = parseAttributeList(); + return 'graph'; } - /** - * Execute a function fn for each pair of elements in two arrays - * @param {Array | *} array1 - * @param {Array | *} array2 - * @param {function} fn - */ - function forEach2(array1, array2, fn) { - if (array1 instanceof Array) { - array1.forEach(function (elem1) { - if (array2 instanceof Array) { - array2.forEach(function (elem2) { - fn(elem1, elem2); - }); - } - else { - fn(elem1, array2); - } - }); - } - else { - if (array2 instanceof Array) { - array2.forEach(function (elem2) { - fn(array1, elem2); - }); - } - else { - fn(array1, array2); - } - } + return null; + } + + /** + * parse a node statement + * @param {Object} graph + * @param {String | Number} id + */ + function parseNodeStatement(graph, id) { + // node statement + var node = { + id: id + }; + var attr = parseAttributeList(); + if (attr) { + node.attr = attr; } + addNode(graph, node); - /** - * Convert a string containing a graph in DOT language into a map containing - * with nodes and edges in the format of graph. - * @param {String} data Text containing a graph in DOT-notation - * @return {Object} graphData - */ - function DOTToGraph (data) { - // parse the DOT file - var dotData = parseDOT(data); - var graphData = { - nodes: [], - edges: [], - options: {} - }; + // edge statements + parseEdge(graph, id); + } - // copy the nodes - if (dotData.nodes) { - dotData.nodes.forEach(function (dotNode) { - var graphNode = { - id: dotNode.id, - label: String(dotNode.label || dotNode.id) - }; - merge(graphNode, dotNode.attr); - if (graphNode.image) { - graphNode.shape = 'image'; - } - graphData.nodes.push(graphNode); - }); + /** + * Parse an edge or a series of edges + * @param {Object} graph + * @param {String | Number} from Id of the from node + */ + function parseEdge(graph, from) { + while (token == '->' || token == '--') { + var to; + var type = token; + getToken(); + + var subgraph = parseSubgraph(graph); + if (subgraph) { + to = subgraph; + } + else { + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError('Identifier or subgraph expected'); } + to = token; + addNode(graph, { + id: to + }); + getToken(); + } - // copy the edges - if (dotData.edges) { - /** - * Convert an edge in DOT format to an edge with VisGraph format - * @param {Object} dotEdge - * @returns {Object} graphEdge - */ - function convertEdge(dotEdge) { - var graphEdge = { - from: dotEdge.from, - to: dotEdge.to - }; - merge(graphEdge, dotEdge.attr); - graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line'; - return graphEdge; - } - - dotData.edges.forEach(function (dotEdge) { - var from, to; - if (dotEdge.from instanceof Object) { - from = dotEdge.from.nodes; - } - else { - from = { - id: dotEdge.from - } - } - - if (dotEdge.to instanceof Object) { - to = dotEdge.to.nodes; - } - else { - to = { - id: dotEdge.to - } - } - - if (dotEdge.from instanceof Object && dotEdge.from.edges) { - dotEdge.from.edges.forEach(function (subEdge) { - var graphEdge = convertEdge(subEdge); - graphData.edges.push(graphEdge); - }); - } - - forEach2(from, to, function (from, to) { - var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr); - var graphEdge = convertEdge(subEdge); - graphData.edges.push(graphEdge); - }); - - if (dotEdge.to instanceof Object && dotEdge.to.edges) { - dotEdge.to.edges.forEach(function (subEdge) { - var graphEdge = convertEdge(subEdge); - graphData.edges.push(graphEdge); - }); - } - }); - } + // parse edge attributes + var attr = parseAttributeList(); - // copy the options - if (dotData.attr) { - graphData.options = dotData.attr; - } + // create edge + var edge = createEdge(graph, from, to, type, attr); + addEdge(graph, edge); - return graphData; + from = to; } + } - // exports - exports.parseDOT = parseDOT; - exports.DOTToGraph = DOTToGraph; - -})(typeof util !== 'undefined' ? util : exports); - -/** - * Canvas shapes used by the Graph - */ -if (typeof CanvasRenderingContext2D !== 'undefined') { - - /** - * Draw a circle shape - */ - CanvasRenderingContext2D.prototype.circle = function(x, y, r) { - this.beginPath(); - this.arc(x, y, r, 0, 2*Math.PI, false); - }; - - /** - * Draw a square shape - * @param {Number} x horizontal center - * @param {Number} y vertical center - * @param {Number} r size, width and height of the square - */ - CanvasRenderingContext2D.prototype.square = function(x, y, r) { - this.beginPath(); - this.rect(x - r, y - r, r * 2, r * 2); - }; - - /** - * Draw a triangle shape - * @param {Number} x horizontal center - * @param {Number} y vertical center - * @param {Number} r radius, half the length of the sides of the triangle - */ - CanvasRenderingContext2D.prototype.triangle = function(x, y, r) { - // http://en.wikipedia.org/wiki/Equilateral_triangle - this.beginPath(); - - var s = r * 2; - var s2 = s / 2; - var ir = Math.sqrt(3) / 6 * s; // radius of inner circle - var h = Math.sqrt(s * s - s2 * s2); // height - - this.moveTo(x, y - (h - ir)); - this.lineTo(x + s2, y + ir); - this.lineTo(x - s2, y + ir); - this.lineTo(x, y - (h - ir)); - this.closePath(); - }; + /** + * Parse a set with attributes, + * for example [label="1.000", shape=solid] + * @return {Object | null} attr + */ + function parseAttributeList() { + var attr = null; + + while (token == '[') { + getToken(); + attr = {}; + while (token !== '' && token != ']') { + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError('Attribute name expected'); + } + var name = token; - /** - * Draw a triangle shape in downward orientation - * @param {Number} x horizontal center - * @param {Number} y vertical center - * @param {Number} r radius - */ - CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) { - // http://en.wikipedia.org/wiki/Equilateral_triangle - this.beginPath(); - - var s = r * 2; - var s2 = s / 2; - var ir = Math.sqrt(3) / 6 * s; // radius of inner circle - var h = Math.sqrt(s * s - s2 * s2); // height - - this.moveTo(x, y + (h - ir)); - this.lineTo(x + s2, y - ir); - this.lineTo(x - s2, y - ir); - this.lineTo(x, y + (h - ir)); - this.closePath(); - }; + getToken(); + if (token != '=') { + throw newSyntaxError('Equal sign = expected'); + } + getToken(); - /** - * Draw a star shape, a star with 5 points - * @param {Number} x horizontal center - * @param {Number} y vertical center - * @param {Number} r radius, half the length of the sides of the triangle - */ - CanvasRenderingContext2D.prototype.star = function(x, y, r) { - // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/ - this.beginPath(); + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError('Attribute value expected'); + } + var value = token; + setValue(attr, name, value); // name can be a path - for (var n = 0; n < 10; n++) { - var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5; - this.lineTo( - x + radius * Math.sin(n * 2 * Math.PI / 10), - y - radius * Math.cos(n * 2 * Math.PI / 10) - ); + getToken(); + if (token ==',') { + getToken(); } + } - this.closePath(); - }; + if (token != ']') { + throw newSyntaxError('Bracket ] expected'); + } + getToken(); + } - /** - * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas - */ - CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) { - var r2d = Math.PI/180; - if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x - if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y - this.beginPath(); - this.moveTo(x+r,y); - this.lineTo(x+w-r,y); - this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false); - this.lineTo(x+w,y+h-r); - this.arc(x+w-r,y+h-r,r,0,r2d*90,false); - this.lineTo(x+r,y+h); - this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false); - this.lineTo(x,y+r); - this.arc(x+r,y+r,r,r2d*180,r2d*270,false); - }; + return attr; + } - /** - * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas - */ - CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) { - var kappa = .5522848, - ox = (w / 2) * kappa, // control point offset horizontal - oy = (h / 2) * kappa, // control point offset vertical - xe = x + w, // x-end - ye = y + h, // y-end - xm = x + w / 2, // x-middle - ym = y + h / 2; // y-middle - - this.beginPath(); - this.moveTo(x, ym); - this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); - this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); - this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); - this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); - }; + /** + * Create a syntax error with extra information on current token and index. + * @param {String} message + * @returns {SyntaxError} err + */ + function newSyntaxError(message) { + return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')'); + } + /** + * Chop off text after a maximum length + * @param {String} text + * @param {Number} maxLength + * @returns {String} + */ + function chop (text, maxLength) { + return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...'); + } + /** + * Execute a function fn for each pair of elements in two arrays + * @param {Array | *} array1 + * @param {Array | *} array2 + * @param {function} fn + */ + function forEach2(array1, array2, fn) { + if (array1 instanceof Array) { + array1.forEach(function (elem1) { + if (array2 instanceof Array) { + array2.forEach(function (elem2) { + fn(elem1, elem2); + }); + } + else { + fn(elem1, array2); + } + }); + } + else { + if (array2 instanceof Array) { + array2.forEach(function (elem2) { + fn(array1, elem2); + }); + } + else { + fn(array1, array2); + } + } + } - /** - * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas - */ - CanvasRenderingContext2D.prototype.database = function(x, y, w, h) { - var f = 1/3; - var wEllipse = w; - var hEllipse = h * f; + /** + * Convert a string containing a graph in DOT language into a map containing + * with nodes and edges in the format of graph. + * @param {String} data Text containing a graph in DOT-notation + * @return {Object} graphData + */ + function DOTToGraph (data) { + // parse the DOT file + var dotData = parseDOT(data); + var graphData = { + nodes: [], + edges: [], + options: {} + }; - var kappa = .5522848, - ox = (wEllipse / 2) * kappa, // control point offset horizontal - oy = (hEllipse / 2) * kappa, // control point offset vertical - xe = x + wEllipse, // x-end - ye = y + hEllipse, // y-end - xm = x + wEllipse / 2, // x-middle - ym = y + hEllipse / 2, // y-middle - ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse - yeb = y + h; // y-end, bottom ellipse + // copy the nodes + if (dotData.nodes) { + dotData.nodes.forEach(function (dotNode) { + var graphNode = { + id: dotNode.id, + label: String(dotNode.label || dotNode.id) + }; + merge(graphNode, dotNode.attr); + if (graphNode.image) { + graphNode.shape = 'image'; + } + graphData.nodes.push(graphNode); + }); + } + + // copy the edges + if (dotData.edges) { + /** + * Convert an edge in DOT format to an edge with VisGraph format + * @param {Object} dotEdge + * @returns {Object} graphEdge + */ + function convertEdge(dotEdge) { + var graphEdge = { + from: dotEdge.from, + to: dotEdge.to + }; + merge(graphEdge, dotEdge.attr); + graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line'; + return graphEdge; + } - this.beginPath(); - this.moveTo(xe, ym); + dotData.edges.forEach(function (dotEdge) { + var from, to; + if (dotEdge.from instanceof Object) { + from = dotEdge.from.nodes; + } + else { + from = { + id: dotEdge.from + } + } - this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); - this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + if (dotEdge.to instanceof Object) { + to = dotEdge.to.nodes; + } + else { + to = { + id: dotEdge.to + } + } - this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); - this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + if (dotEdge.from instanceof Object && dotEdge.from.edges) { + dotEdge.from.edges.forEach(function (subEdge) { + var graphEdge = convertEdge(subEdge); + graphData.edges.push(graphEdge); + }); + } - this.lineTo(xe, ymb); + forEach2(from, to, function (from, to) { + var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr); + var graphEdge = convertEdge(subEdge); + graphData.edges.push(graphEdge); + }); - this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb); - this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb); + if (dotEdge.to instanceof Object && dotEdge.to.edges) { + dotEdge.to.edges.forEach(function (subEdge) { + var graphEdge = convertEdge(subEdge); + graphData.edges.push(graphEdge); + }); + } + }); + } - this.lineTo(x, ym); - }; + // copy the options + if (dotData.attr) { + graphData.options = dotData.attr; + } + return graphData; + } - /** - * Draw an arrow point (no line) - */ - CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) { - // tail - var xt = x - length * Math.cos(angle); - var yt = y - length * Math.sin(angle); - - // inner tail - // TODO: allow to customize different shapes - var xi = x - length * 0.9 * Math.cos(angle); - var yi = y - length * 0.9 * Math.sin(angle); - - // left - var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI); - var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI); - - // right - var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI); - var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI); - - this.beginPath(); - this.moveTo(x, y); - this.lineTo(xl, yl); - this.lineTo(xi, yi); - this.lineTo(xr, yr); - this.closePath(); - }; + // exports + exports.parseDOT = parseDOT; + exports.DOTToGraph = DOTToGraph; - /** - * Sets up the dashedLine functionality for drawing - * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas - * @author David Jordan - * @date 2012-08-08 - */ - CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){ - if (!dashArray) dashArray=[10,5]; - if (dashLength==0) dashLength = 0.001; // Hack for Safari - var dashCount = dashArray.length; - this.moveTo(x, y); - var dx = (x2-x), dy = (y2-y); - var slope = dy/dx; - var distRemaining = Math.sqrt( dx*dx + dy*dy ); - var dashIndex=0, draw=true; - while (distRemaining>=0.1){ - var dashLength = dashArray[dashIndex++%dashCount]; - if (dashLength > distRemaining) dashLength = distRemaining; - var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) ); - if (dx<0) xStep = -xStep; - x += xStep; - y += slope*xStep; - this[draw ? 'lineTo' : 'moveTo'](x,y); - distRemaining -= dashLength; - draw = !draw; - } - }; +})(typeof util !== 'undefined' ? util : exports); - // TODO: add diamond shape +/** + * Canvas shapes used by the Graph + */ +if (typeof CanvasRenderingContext2D !== 'undefined') { + + /** + * Draw a circle shape + */ + CanvasRenderingContext2D.prototype.circle = function(x, y, r) { + this.beginPath(); + this.arc(x, y, r, 0, 2*Math.PI, false); + }; + + /** + * Draw a square shape + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r size, width and height of the square + */ + CanvasRenderingContext2D.prototype.square = function(x, y, r) { + this.beginPath(); + this.rect(x - r, y - r, r * 2, r * 2); + }; + + /** + * Draw a triangle shape + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r radius, half the length of the sides of the triangle + */ + CanvasRenderingContext2D.prototype.triangle = function(x, y, r) { + // http://en.wikipedia.org/wiki/Equilateral_triangle + this.beginPath(); + + var s = r * 2; + var s2 = s / 2; + var ir = Math.sqrt(3) / 6 * s; // radius of inner circle + var h = Math.sqrt(s * s - s2 * s2); // height + + this.moveTo(x, y - (h - ir)); + this.lineTo(x + s2, y + ir); + this.lineTo(x - s2, y + ir); + this.lineTo(x, y - (h - ir)); + this.closePath(); + }; + + /** + * Draw a triangle shape in downward orientation + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r radius + */ + CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) { + // http://en.wikipedia.org/wiki/Equilateral_triangle + this.beginPath(); + + var s = r * 2; + var s2 = s / 2; + var ir = Math.sqrt(3) / 6 * s; // radius of inner circle + var h = Math.sqrt(s * s - s2 * s2); // height + + this.moveTo(x, y + (h - ir)); + this.lineTo(x + s2, y - ir); + this.lineTo(x - s2, y - ir); + this.lineTo(x, y + (h - ir)); + this.closePath(); + }; + + /** + * Draw a star shape, a star with 5 points + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r radius, half the length of the sides of the triangle + */ + CanvasRenderingContext2D.prototype.star = function(x, y, r) { + // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/ + this.beginPath(); + + for (var n = 0; n < 10; n++) { + var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5; + this.lineTo( + x + radius * Math.sin(n * 2 * Math.PI / 10), + y - radius * Math.cos(n * 2 * Math.PI / 10) + ); + } + + this.closePath(); + }; + + /** + * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas + */ + CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) { + var r2d = Math.PI/180; + if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x + if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y + this.beginPath(); + this.moveTo(x+r,y); + this.lineTo(x+w-r,y); + this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false); + this.lineTo(x+w,y+h-r); + this.arc(x+w-r,y+h-r,r,0,r2d*90,false); + this.lineTo(x+r,y+h); + this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false); + this.lineTo(x,y+r); + this.arc(x+r,y+r,r,r2d*180,r2d*270,false); + }; + + /** + * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas + */ + CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) { + var kappa = .5522848, + ox = (w / 2) * kappa, // control point offset horizontal + oy = (h / 2) * kappa, // control point offset vertical + xe = x + w, // x-end + ye = y + h, // y-end + xm = x + w / 2, // x-middle + ym = y + h / 2; // y-middle + + this.beginPath(); + this.moveTo(x, ym); + this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + }; + + + + /** + * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas + */ + CanvasRenderingContext2D.prototype.database = function(x, y, w, h) { + var f = 1/3; + var wEllipse = w; + var hEllipse = h * f; + + var kappa = .5522848, + ox = (wEllipse / 2) * kappa, // control point offset horizontal + oy = (hEllipse / 2) * kappa, // control point offset vertical + xe = x + wEllipse, // x-end + ye = y + hEllipse, // y-end + xm = x + wEllipse / 2, // x-middle + ym = y + hEllipse / 2, // y-middle + ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse + yeb = y + h; // y-end, bottom ellipse + + this.beginPath(); + this.moveTo(xe, ym); + + this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + + this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + + this.lineTo(xe, ymb); + + this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb); + this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb); + + this.lineTo(x, ym); + }; + + + /** + * Draw an arrow point (no line) + */ + CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) { + // tail + var xt = x - length * Math.cos(angle); + var yt = y - length * Math.sin(angle); + + // inner tail + // TODO: allow to customize different shapes + var xi = x - length * 0.9 * Math.cos(angle); + var yi = y - length * 0.9 * Math.sin(angle); + + // left + var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI); + var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI); + + // right + var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI); + var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI); + + this.beginPath(); + this.moveTo(x, y); + this.lineTo(xl, yl); + this.lineTo(xi, yi); + this.lineTo(xr, yr); + this.closePath(); + }; + + /** + * Sets up the dashedLine functionality for drawing + * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas + * @author David Jordan + * @date 2012-08-08 + */ + CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){ + if (!dashArray) dashArray=[10,5]; + if (dashLength==0) dashLength = 0.001; // Hack for Safari + var dashCount = dashArray.length; + this.moveTo(x, y); + var dx = (x2-x), dy = (y2-y); + var slope = dy/dx; + var distRemaining = Math.sqrt( dx*dx + dy*dy ); + var dashIndex=0, draw=true; + while (distRemaining>=0.1){ + var dashLength = dashArray[dashIndex++%dashCount]; + if (dashLength > distRemaining) dashLength = distRemaining; + var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) ); + if (dx<0) xStep = -xStep; + x += xStep; + y += slope*xStep; + this[draw ? 'lineTo' : 'moveTo'](x,y); + distRemaining -= dashLength; + draw = !draw; + } + }; + + // TODO: add diamond shape } /** @@ -12404,43 +11883,43 @@ if (typeof CanvasRenderingContext2D !== 'undefined') { * example for the color */ function Node(properties, imagelist, grouplist, constants) { - this.selected = false; + this.selected = false; - this.edges = []; // all edges connected to this node - this.group = constants.nodes.group; + this.edges = []; // all edges connected to this node + this.group = constants.nodes.group; - this.fontSize = constants.nodes.fontSize; - this.fontFace = constants.nodes.fontFace; - this.fontColor = constants.nodes.fontColor; + this.fontSize = constants.nodes.fontSize; + this.fontFace = constants.nodes.fontFace; + this.fontColor = constants.nodes.fontColor; - this.color = constants.nodes.color; + this.color = constants.nodes.color; - // set defaults for the properties - this.id = undefined; - this.shape = constants.nodes.shape; - this.image = constants.nodes.image; - this.x = 0; - this.y = 0; - this.xFixed = false; - this.yFixed = false; - this.radius = constants.nodes.radius; - this.radiusFixed = false; - this.radiusMin = constants.nodes.radiusMin; - this.radiusMax = constants.nodes.radiusMax; + // set defaults for the properties + this.id = undefined; + this.shape = constants.nodes.shape; + this.image = constants.nodes.image; + this.x = 0; + this.y = 0; + this.xFixed = false; + this.yFixed = false; + this.radius = constants.nodes.radius; + this.radiusFixed = false; + this.radiusMin = constants.nodes.radiusMin; + this.radiusMax = constants.nodes.radiusMax; - this.imagelist = imagelist; - this.grouplist = grouplist; + this.imagelist = imagelist; + this.grouplist = grouplist; - this.setProperties(properties, constants); + this.setProperties(properties, constants); - // mass, force, velocity - this.mass = 50; // kg (mass is adjusted for the number of connected edges) - this.fx = 0.0; // external force x - this.fy = 0.0; // external force y - this.vx = 0.0; // velocity x - this.vy = 0.0; // velocity y - this.minForce = constants.minForce; - this.damping = 0.9; // damping factor + // mass, force, velocity + this.mass = 50; // kg (mass is adjusted for the number of connected edges) + this.fx = 0.0; // external force x + this.fy = 0.0; // external force y + this.vx = 0.0; // velocity x + this.vy = 0.0; // velocity y + this.minForce = constants.minForce; + this.damping = 0.9; // damping factor }; /** @@ -12448,10 +11927,10 @@ function Node(properties, imagelist, grouplist, constants) { * @param {Edge} edge */ Node.prototype.attachEdge = function(edge) { - if (this.edges.indexOf(edge) == -1) { - this.edges.push(edge); - } - this._updateMass(); + if (this.edges.indexOf(edge) == -1) { + this.edges.push(edge); + } + this._updateMass(); }; /** @@ -12459,11 +11938,11 @@ Node.prototype.attachEdge = function(edge) { * @param {Edge} edge */ Node.prototype.detachEdge = function(edge) { - var index = this.edges.indexOf(edge); - if (index != -1) { - this.edges.splice(index, 1); - } - this._updateMass(); + var index = this.edges.indexOf(edge); + if (index != -1) { + this.edges.splice(index, 1); + } + this._updateMass(); }; /** @@ -12472,7 +11951,7 @@ Node.prototype.detachEdge = function(edge) { * @private */ Node.prototype._updateMass = function() { - this.mass = 50 + 20 * this.edges.length; // kg + this.mass = 50 + 20 * this.edges.length; // kg }; /** @@ -12481,81 +11960,81 @@ Node.prototype._updateMass = function() { * @param {Object} constants and object with default, global properties */ Node.prototype.setProperties = function(properties, constants) { - if (!properties) { - return; - } - - // basic properties - if (properties.id != undefined) {this.id = properties.id;} - if (properties.label != undefined) {this.label = properties.label;} - if (properties.title != undefined) {this.title = properties.title;} - if (properties.group != undefined) {this.group = properties.group;} - if (properties.x != undefined) {this.x = properties.x;} - if (properties.y != undefined) {this.y = properties.y;} - if (properties.value != undefined) {this.value = properties.value;} + if (!properties) { + return; + } - if (this.id === undefined) { - throw "Node must have an id"; - } + // basic properties + if (properties.id != undefined) {this.id = properties.id;} + if (properties.label != undefined) {this.label = properties.label;} + if (properties.title != undefined) {this.title = properties.title;} + if (properties.group != undefined) {this.group = properties.group;} + if (properties.x != undefined) {this.x = properties.x;} + if (properties.y != undefined) {this.y = properties.y;} + if (properties.value != undefined) {this.value = properties.value;} + + if (this.id === undefined) { + throw "Node must have an id"; + } - // copy group properties - if (this.group) { - var groupObj = this.grouplist.get(this.group); - for (var prop in groupObj) { - if (groupObj.hasOwnProperty(prop)) { - this[prop] = groupObj[prop]; - } - } + // copy group properties + if (this.group) { + var groupObj = this.grouplist.get(this.group); + for (var prop in groupObj) { + if (groupObj.hasOwnProperty(prop)) { + this[prop] = groupObj[prop]; + } } + } - // individual shape properties - if (properties.shape != undefined) {this.shape = properties.shape;} - if (properties.image != undefined) {this.image = properties.image;} - if (properties.radius != undefined) {this.radius = properties.radius;} - if (properties.color != undefined) {this.color = Node.parseColor(properties.color);} + // individual shape properties + if (properties.shape != undefined) {this.shape = properties.shape;} + if (properties.image != undefined) {this.image = properties.image;} + if (properties.radius != undefined) {this.radius = properties.radius;} + if (properties.color != undefined) {this.color = Node.parseColor(properties.color);} - if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;} - if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;} - if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;} + if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;} + if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;} + if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;} - if (this.image != undefined) { - if (this.imagelist) { - this.imageObj = this.imagelist.load(this.image); - } - else { - throw "No imagelist provided"; - } + if (this.image != undefined) { + if (this.imagelist) { + this.imageObj = this.imagelist.load(this.image); + } + else { + throw "No imagelist provided"; } + } - this.xFixed = this.xFixed || (properties.x != undefined); - this.yFixed = this.yFixed || (properties.y != undefined); - this.radiusFixed = this.radiusFixed || (properties.radius != undefined); + this.xFixed = this.xFixed || (properties.x != undefined); + this.yFixed = this.yFixed || (properties.y != undefined); + this.radiusFixed = this.radiusFixed || (properties.radius != undefined); - if (this.shape == 'image') { - this.radiusMin = constants.nodes.widthMin; - this.radiusMax = constants.nodes.widthMax; - } + if (this.shape == 'image') { + this.radiusMin = constants.nodes.widthMin; + this.radiusMax = constants.nodes.widthMax; + } - // choose draw method depending on the shape - switch (this.shape) { - case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break; - case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break; - case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break; - case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break; - // TODO: add diamond shape - case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break; - case 'text': this.draw = this._drawText; this.resize = this._resizeText; break; - case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break; - case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break; - case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break; - case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break; - case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break; - default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break; - } + // choose draw method depending on the shape + switch (this.shape) { + case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break; + case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break; + case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break; + case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break; + // TODO: add diamond shape + case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break; + case 'text': this.draw = this._drawText; this.resize = this._resizeText; break; + case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break; + case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break; + case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break; + case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break; + case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break; + default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break; + } - // reset the size of the node, this can be changed - this._reset(); + // reset the size of the node, this can be changed + this._reset(); }; /** @@ -12565,51 +12044,51 @@ Node.prototype.setProperties = function(properties, constants) { * @return {Object} colorObject */ Node.parseColor = function(color) { - var c; - if (util.isString(color)) { - c = { - border: color, - background: color, - highlight: { - border: color, - background: color - } - }; - // TODO: automatically generate a nice highlight color + var c; + if (util.isString(color)) { + c = { + border: color, + background: color, + highlight: { + border: color, + background: color + } + }; + // TODO: automatically generate a nice highlight color + } + else { + c = {}; + c.background = color.background || 'white'; + c.border = color.border || c.background; + if (util.isString(color.highlight)) { + c.highlight = { + border: color.highlight, + background: color.highlight + } } else { - c = {}; - c.background = color.background || 'white'; - c.border = color.border || c.background; - if (util.isString(color.highlight)) { - c.highlight = { - border: color.highlight, - background: color.highlight - } - } - else { - c.highlight = {}; - c.highlight.background = color.highlight && color.highlight.background || c.background; - c.highlight.border = color.highlight && color.highlight.border || c.border; - } + c.highlight = {}; + c.highlight.background = color.highlight && color.highlight.background || c.background; + c.highlight.border = color.highlight && color.highlight.border || c.border; } - return c; + } + return c; }; /** * select this node */ Node.prototype.select = function() { - this.selected = true; - this._reset(); + this.selected = true; + this._reset(); }; /** * unselect this node */ Node.prototype.unselect = function() { - this.selected = false; - this._reset(); + this.selected = false; + this._reset(); }; /** @@ -12617,8 +12096,8 @@ Node.prototype.unselect = function() { * @private */ Node.prototype._reset = function() { - this.width = undefined; - this.height = undefined; + this.width = undefined; + this.height = undefined; }; /** @@ -12627,7 +12106,7 @@ Node.prototype._reset = function() { * has been set. */ Node.prototype.getTitle = function() { - return this.title; + return this.title; }; /** @@ -12637,46 +12116,46 @@ Node.prototype.getTitle = function() { * @returns {number} distance Distance to the border in pixels */ Node.prototype.distanceToBorder = function (ctx, angle) { - var borderWidth = 1; + var borderWidth = 1; - if (!this.width) { - this.resize(ctx); - } + if (!this.width) { + this.resize(ctx); + } - //noinspection FallthroughInSwitchStatementJS - switch (this.shape) { - case 'circle': - case 'dot': - return this.radius + borderWidth; - - case 'ellipse': - 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); - - // TODO: implement distanceToBorder for database - // TODO: implement distanceToBorder for triangle - // TODO: implement distanceToBorder for triangleDown - - case 'box': - case 'image': - case 'text': - default: - if (this.width) { - return Math.min( - Math.abs(this.width / 2 / Math.cos(angle)), - Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth; - // TODO: reckon with border radius too in case of box - } - else { - return 0; - } + //noinspection FallthroughInSwitchStatementJS + switch (this.shape) { + case 'circle': + case 'dot': + return this.radius + borderWidth; + + case 'ellipse': + 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); + + // TODO: implement distanceToBorder for database + // TODO: implement distanceToBorder for triangle + // TODO: implement distanceToBorder for triangleDown + + case 'box': + case 'image': + case 'text': + default: + if (this.width) { + return Math.min( + Math.abs(this.width / 2 / Math.cos(angle)), + Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth; + // TODO: reckon with border radius too in case of box + } + else { + return 0; + } - } + } - // TODO: implement calculation of distance to border for all shapes + // TODO: implement calculation of distance to border for all shapes }; /** @@ -12685,8 +12164,8 @@ Node.prototype.distanceToBorder = function (ctx, angle) { * @param {number} fy Force in vertical direction */ Node.prototype._setForce = function(fx, fy) { - this.fx = fx; - this.fy = fy; + this.fx = fx; + this.fy = fy; }; /** @@ -12696,8 +12175,8 @@ Node.prototype._setForce = function(fx, fy) { * @private */ Node.prototype._addForce = function(fx, fy) { - this.fx += fx; - this.fy += fy; + this.fx += fx; + this.fy += fy; }; /** @@ -12705,19 +12184,19 @@ Node.prototype._addForce = function(fx, fy) { * @param {number} interval Time interval in seconds */ Node.prototype.discreteStep = function(interval) { - if (!this.xFixed) { - var dx = -this.damping * this.vx; // damping force - var ax = (this.fx + dx) / this.mass; // acceleration - this.vx += ax / interval; // velocity - this.x += this.vx / interval; // position - } + if (!this.xFixed) { + var dx = -this.damping * this.vx; // damping force + var ax = (this.fx + dx) / this.mass; // acceleration + this.vx += ax / interval; // velocity + this.x += this.vx / interval; // position + } - if (!this.yFixed) { - var dy = -this.damping * this.vy; // damping force - var ay = (this.fy + dy) / this.mass; // acceleration - this.vy += ay / interval; // velocity - this.y += this.vy / interval; // position - } + if (!this.yFixed) { + var dy = -this.damping * this.vy; // damping force + var ay = (this.fy + dy) / this.mass; // acceleration + this.vy += ay / interval; // velocity + this.y += this.vy / interval; // position + } }; @@ -12726,7 +12205,7 @@ Node.prototype.discreteStep = function(interval) { * @return {boolean} true if fixed, false if not */ Node.prototype.isFixed = function() { - return (this.xFixed && this.yFixed); + return (this.xFixed && this.yFixed); }; /** @@ -12736,9 +12215,9 @@ Node.prototype.isFixed = function() { */ // TODO: replace this method with calculating the kinetic energy Node.prototype.isMoving = function(vmin) { - return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin || - (!this.xFixed && Math.abs(this.fx) > this.minForce) || - (!this.yFixed && Math.abs(this.fy) > this.minForce)); + return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin || + (!this.xFixed && Math.abs(this.fx) > this.minForce) || + (!this.yFixed && Math.abs(this.fy) > this.minForce)); }; /** @@ -12746,7 +12225,7 @@ Node.prototype.isMoving = function(vmin) { * @return {boolean} selected True if node is selected, else false */ Node.prototype.isSelected = function() { - return this.selected; + return this.selected; }; /** @@ -12754,7 +12233,7 @@ Node.prototype.isSelected = function() { * @return {Number} value */ Node.prototype.getValue = function() { - return this.value; + return this.value; }; /** @@ -12764,9 +12243,9 @@ Node.prototype.getValue = function() { * @return {Number} value */ Node.prototype.getDistance = function(x, y) { - var dx = this.x - x, - dy = this.y - y; - return Math.sqrt(dx * dx + dy * dy); + var dx = this.x - x, + dy = this.y - y; + return Math.sqrt(dx * dx + dy * dy); }; @@ -12777,15 +12256,15 @@ Node.prototype.getDistance = function(x, y) { * @param {Number} max */ Node.prototype.setValueRange = function(min, max) { - if (!this.radiusFixed && this.value !== undefined) { - if (max == min) { - this.radius = (this.radiusMin + this.radiusMax) / 2; - } - else { - var scale = (this.radiusMax - this.radiusMin) / (max - min); - this.radius = (this.value - min) * scale + this.radiusMin; - } + if (!this.radiusFixed && this.value !== undefined) { + if (max == min) { + this.radius = (this.radiusMin + this.radiusMax) / 2; + } + else { + var scale = (this.radiusMax - this.radiusMin) / (max - min); + this.radius = (this.value - min) * scale + this.radiusMin; } + } }; /** @@ -12794,7 +12273,7 @@ Node.prototype.setValueRange = function(min, max) { * @param {CanvasRenderingContext2D} ctx */ Node.prototype.draw = function(ctx) { - throw "Draw method not initialized for node"; + throw "Draw method not initialized for node"; }; /** @@ -12803,7 +12282,7 @@ Node.prototype.draw = function(ctx) { * @param {CanvasRenderingContext2D} ctx */ Node.prototype.resize = function(ctx) { - throw "Resize method not initialized for node"; + throw "Resize method not initialized for node"; }; /** @@ -12812,258 +12291,258 @@ Node.prototype.resize = function(ctx) { * @return {boolean} True if location is located on node */ Node.prototype.isOverlappingWith = function(obj) { - return (this.left < obj.right && - this.left + this.width > obj.left && - this.top < obj.bottom && - this.top + this.height > obj.top); + return (this.left < obj.right && + this.left + this.width > obj.left && + this.top < obj.bottom && + this.top + this.height > obj.top); }; Node.prototype._resizeImage = function (ctx) { - // TODO: pre calculate the image size - if (!this.width) { // undefined or 0 - var width, height; - if (this.value) { - var scale = this.imageObj.height / this.imageObj.width; - width = this.radius || this.imageObj.width; - height = this.radius * scale || this.imageObj.height; - } - else { - width = this.imageObj.width; - height = this.imageObj.height; - } - this.width = width; - this.height = height; + // TODO: pre calculate the image size + if (!this.width) { // undefined or 0 + var width, height; + if (this.value) { + var scale = this.imageObj.height / this.imageObj.width; + width = this.radius || this.imageObj.width; + height = this.radius * scale || this.imageObj.height; } + else { + width = this.imageObj.width; + height = this.imageObj.height; + } + this.width = width; + this.height = height; + } }; Node.prototype._drawImage = function (ctx) { - this._resizeImage(ctx); + this._resizeImage(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; - var yLabel; - if (this.imageObj) { - ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height); - yLabel = this.y + this.height / 2; - } - else { - // image still loading... just draw the label for now - yLabel = this.y; - } + var yLabel; + if (this.imageObj) { + ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height); + yLabel = this.y + this.height / 2; + } + else { + // image still loading... just draw the label for now + yLabel = this.y; + } - this._label(ctx, this.label, this.x, yLabel, undefined, "top"); + this._label(ctx, this.label, this.x, yLabel, undefined, "top"); }; Node.prototype._resizeBox = function (ctx) { - if (!this.width) { - var margin = 5; - var textSize = this.getTextSize(ctx); - this.width = textSize.width + 2 * margin; - this.height = textSize.height + 2 * margin; - } + if (!this.width) { + var margin = 5; + var textSize = this.getTextSize(ctx); + this.width = textSize.width + 2 * margin; + this.height = textSize.height + 2 * margin; + } }; Node.prototype._drawBox = function (ctx) { - this._resizeBox(ctx); + this._resizeBox(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; - ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; - ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; - ctx.lineWidth = this.selected ? 2.0 : 1.0; - ctx.roundRect(this.left, this.top, this.width, this.height, this.radius); - ctx.fill(); - ctx.stroke(); + ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; + ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; + ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.roundRect(this.left, this.top, this.width, this.height, this.radius); + ctx.fill(); + ctx.stroke(); - this._label(ctx, this.label, this.x, this.y); + this._label(ctx, this.label, this.x, this.y); }; Node.prototype._resizeDatabase = function (ctx) { - if (!this.width) { - var margin = 5; - var textSize = this.getTextSize(ctx); - var size = textSize.width + 2 * margin; - this.width = size; - this.height = size; - } + if (!this.width) { + var margin = 5; + var textSize = this.getTextSize(ctx); + var size = textSize.width + 2 * margin; + this.width = size; + this.height = size; + } }; Node.prototype._drawDatabase = function (ctx) { - this._resizeDatabase(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; - - ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; - ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; - ctx.lineWidth = this.selected ? 2.0 : 1.0; - ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height); - ctx.fill(); - ctx.stroke(); + this._resizeDatabase(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; - this._label(ctx, this.label, this.x, this.y); + ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; + ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; + ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height); + ctx.fill(); + ctx.stroke(); + + this._label(ctx, this.label, this.x, this.y); }; Node.prototype._resizeCircle = function (ctx) { - if (!this.width) { - var margin = 5; - var textSize = this.getTextSize(ctx); - var diameter = Math.max(textSize.width, textSize.height) + 2 * margin; - this.radius = diameter / 2; - - this.width = diameter; - this.height = diameter; - } + if (!this.width) { + var margin = 5; + var textSize = this.getTextSize(ctx); + var diameter = Math.max(textSize.width, textSize.height) + 2 * margin; + this.radius = diameter / 2; + + this.width = diameter; + this.height = diameter; + } }; Node.prototype._drawCircle = function (ctx) { - this._resizeCircle(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; - - ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; - ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; - ctx.lineWidth = this.selected ? 2.0 : 1.0; - ctx.circle(this.x, this.y, this.radius); - ctx.fill(); - ctx.stroke(); + this._resizeCircle(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; - this._label(ctx, this.label, this.x, this.y); + ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; + ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; + ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.circle(this.x, this.y, this.radius); + ctx.fill(); + ctx.stroke(); + + this._label(ctx, this.label, this.x, this.y); }; Node.prototype._resizeEllipse = function (ctx) { - if (!this.width) { - var textSize = this.getTextSize(ctx); + if (!this.width) { + var textSize = this.getTextSize(ctx); - this.width = textSize.width * 1.5; - this.height = textSize.height * 2; - if (this.width < this.height) { - this.width = this.height; - } + this.width = textSize.width * 1.5; + this.height = textSize.height * 2; + if (this.width < this.height) { + this.width = this.height; } + } }; Node.prototype._drawEllipse = function (ctx) { - this._resizeEllipse(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; - - ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; - ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; - ctx.lineWidth = this.selected ? 2.0 : 1.0; - ctx.ellipse(this.left, this.top, this.width, this.height); - ctx.fill(); - ctx.stroke(); + this._resizeEllipse(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; + ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; + ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.ellipse(this.left, this.top, this.width, this.height); + ctx.fill(); + ctx.stroke(); - this._label(ctx, this.label, this.x, this.y); + this._label(ctx, this.label, this.x, this.y); }; Node.prototype._drawDot = function (ctx) { - this._drawShape(ctx, 'circle'); + this._drawShape(ctx, 'circle'); }; Node.prototype._drawTriangle = function (ctx) { - this._drawShape(ctx, 'triangle'); + this._drawShape(ctx, 'triangle'); }; Node.prototype._drawTriangleDown = function (ctx) { - this._drawShape(ctx, 'triangleDown'); + this._drawShape(ctx, 'triangleDown'); }; Node.prototype._drawSquare = function (ctx) { - this._drawShape(ctx, 'square'); + this._drawShape(ctx, 'square'); }; Node.prototype._drawStar = function (ctx) { - this._drawShape(ctx, 'star'); + this._drawShape(ctx, 'star'); }; Node.prototype._resizeShape = function (ctx) { - if (!this.width) { - var size = 2 * this.radius; - this.width = size; - this.height = size; - } + if (!this.width) { + var size = 2 * this.radius; + this.width = size; + this.height = size; + } }; Node.prototype._drawShape = function (ctx, shape) { - this._resizeShape(ctx); + this._resizeShape(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; - ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; - ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; - ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; + ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; + ctx.lineWidth = this.selected ? 2.0 : 1.0; - ctx[shape](this.x, this.y, this.radius); - ctx.fill(); - ctx.stroke(); + ctx[shape](this.x, this.y, this.radius); + ctx.fill(); + ctx.stroke(); - if (this.label) { - this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top'); - } + if (this.label) { + this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top'); + } }; Node.prototype._resizeText = function (ctx) { - if (!this.width) { - var margin = 5; - var textSize = this.getTextSize(ctx); - this.width = textSize.width + 2 * margin; - this.height = textSize.height + 2 * margin; - } + if (!this.width) { + var margin = 5; + var textSize = this.getTextSize(ctx); + this.width = textSize.width + 2 * margin; + this.height = textSize.height + 2 * margin; + } }; Node.prototype._drawText = function (ctx) { - this._resizeText(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; + this._resizeText(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; - this._label(ctx, this.label, this.x, this.y); + this._label(ctx, this.label, this.x, this.y); }; Node.prototype._label = function (ctx, text, x, y, align, baseline) { - if (text) { - ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace; - ctx.fillStyle = this.fontColor || "black"; - ctx.textAlign = align || "center"; - ctx.textBaseline = baseline || "middle"; + if (text) { + ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace; + ctx.fillStyle = this.fontColor || "black"; + ctx.textAlign = align || "center"; + ctx.textBaseline = baseline || "middle"; - var lines = text.split('\n'), - lineCount = lines.length, - fontSize = (this.fontSize + 4), - yLine = y + (1 - lineCount) / 2 * fontSize; + var lines = text.split('\n'), + lineCount = lines.length, + fontSize = (this.fontSize + 4), + yLine = y + (1 - lineCount) / 2 * fontSize; - for (var i = 0; i < lineCount; i++) { - ctx.fillText(lines[i], x, yLine); - yLine += fontSize; - } + for (var i = 0; i < lineCount; i++) { + ctx.fillText(lines[i], x, yLine); + yLine += fontSize; } + } }; Node.prototype.getTextSize = function(ctx) { - if (this.label != undefined) { - ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace; - - var lines = this.label.split('\n'), - height = (this.fontSize + 4) * lines.length, - width = 0; + if (this.label != undefined) { + ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace; - for (var i = 0, iMax = lines.length; i < iMax; i++) { - width = Math.max(width, ctx.measureText(lines[i]).width); - } + var lines = this.label.split('\n'), + height = (this.fontSize + 4) * lines.length, + width = 0; - return {"width": width, "height": height}; - } - else { - return {"width": 0, "height": 0}; + for (var i = 0, iMax = lines.length; i < iMax; i++) { + width = Math.max(width, ctx.measureText(lines[i]).width); } + + return {"width": width, "height": height}; + } + else { + return {"width": 0, "height": 0}; + } }; /** @@ -13082,40 +12561,40 @@ Node.prototype.getTextSize = function(ctx) { * example for the color */ function Edge (properties, graph, constants) { - if (!graph) { - throw "No graph provided"; - } - this.graph = graph; - - // initialize constants - this.widthMin = constants.edges.widthMin; - this.widthMax = constants.edges.widthMax; - - // initialize variables - this.id = undefined; - this.fromId = undefined; - this.toId = undefined; - this.style = constants.edges.style; - this.title = undefined; - this.width = constants.edges.width; - this.value = undefined; - this.length = constants.edges.length; - - this.from = null; // a node - this.to = null; // a node - this.connected = false; - - // Added to support dashed lines - // David Jordan - // 2012-08-08 - this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength - - this.stiffness = undefined; // depends on the length of the edge - this.color = constants.edges.color; - this.widthFixed = false; - this.lengthFixed = false; - - this.setProperties(properties, constants); + if (!graph) { + throw "No graph provided"; + } + this.graph = graph; + + // initialize constants + this.widthMin = constants.edges.widthMin; + this.widthMax = constants.edges.widthMax; + + // initialize variables + this.id = undefined; + this.fromId = undefined; + this.toId = undefined; + this.style = constants.edges.style; + this.title = undefined; + this.width = constants.edges.width; + this.value = undefined; + this.length = constants.edges.length; + + this.from = null; // a node + this.to = null; // a node + this.connected = false; + + // Added to support dashed lines + // David Jordan + // 2012-08-08 + this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength + + this.stiffness = undefined; // depends on the length of the edge + this.color = constants.edges.color; + this.widthFixed = false; + this.lengthFixed = false; + + this.setProperties(properties, constants); } /** @@ -13124,95 +12603,95 @@ function Edge (properties, graph, constants) { * @param {Object} constants and object with default, global properties */ Edge.prototype.setProperties = function(properties, constants) { - if (!properties) { - return; - } - - if (properties.from != undefined) {this.fromId = properties.from;} - if (properties.to != undefined) {this.toId = properties.to;} + if (!properties) { + return; + } - if (properties.id != undefined) {this.id = properties.id;} - if (properties.style != undefined) {this.style = properties.style;} - if (properties.label != undefined) {this.label = properties.label;} - if (this.label) { - this.fontSize = constants.edges.fontSize; - this.fontFace = constants.edges.fontFace; - this.fontColor = constants.edges.fontColor; - if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;} - if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;} - if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;} - } - if (properties.title != undefined) {this.title = properties.title;} - if (properties.width != undefined) {this.width = properties.width;} - if (properties.value != undefined) {this.value = properties.value;} - if (properties.length != undefined) {this.length = properties.length;} + if (properties.from != undefined) {this.fromId = properties.from;} + if (properties.to != undefined) {this.toId = properties.to;} + + if (properties.id != undefined) {this.id = properties.id;} + if (properties.style != undefined) {this.style = properties.style;} + if (properties.label != undefined) {this.label = properties.label;} + if (this.label) { + this.fontSize = constants.edges.fontSize; + this.fontFace = constants.edges.fontFace; + this.fontColor = constants.edges.fontColor; + if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;} + if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;} + if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;} + } + if (properties.title != undefined) {this.title = properties.title;} + if (properties.width != undefined) {this.width = properties.width;} + if (properties.value != undefined) {this.value = properties.value;} + if (properties.length != undefined) {this.length = properties.length;} + + // Added to support dashed lines + // David Jordan + // 2012-08-08 + if (properties.dash) { + if (properties.dash.length != undefined) {this.dash.length = properties.dash.length;} + if (properties.dash.gap != undefined) {this.dash.gap = properties.dash.gap;} + if (properties.dash.altLength != undefined) {this.dash.altLength = properties.dash.altLength;} + } - // Added to support dashed lines - // David Jordan - // 2012-08-08 - if (properties.dash) { - if (properties.dash.length != undefined) {this.dash.length = properties.dash.length;} - if (properties.dash.gap != undefined) {this.dash.gap = properties.dash.gap;} - if (properties.dash.altLength != undefined) {this.dash.altLength = properties.dash.altLength;} - } - - if (properties.color != undefined) {this.color = properties.color;} + if (properties.color != undefined) {this.color = properties.color;} - // A node is connected when it has a from and to node. - this.connect(); + // A node is connected when it has a from and to node. + this.connect(); - this.widthFixed = this.widthFixed || (properties.width != undefined); - this.lengthFixed = this.lengthFixed || (properties.length != undefined); - this.stiffness = 1 / this.length; + this.widthFixed = this.widthFixed || (properties.width != undefined); + this.lengthFixed = this.lengthFixed || (properties.length != undefined); + this.stiffness = 1 / this.length; - // set draw method based on style - switch (this.style) { - case 'line': this.draw = this._drawLine; break; - case 'arrow': this.draw = this._drawArrow; break; - case 'arrow-center': this.draw = this._drawArrowCenter; break; - case 'dash-line': this.draw = this._drawDashLine; break; - default: this.draw = this._drawLine; break; - } + // set draw method based on style + switch (this.style) { + case 'line': this.draw = this._drawLine; break; + case 'arrow': this.draw = this._drawArrow; break; + case 'arrow-center': this.draw = this._drawArrowCenter; break; + case 'dash-line': this.draw = this._drawDashLine; break; + default: this.draw = this._drawLine; break; + } }; /** * Connect an edge to its nodes */ Edge.prototype.connect = function () { - this.disconnect(); + this.disconnect(); - this.from = this.graph.nodes[this.fromId] || null; - this.to = this.graph.nodes[this.toId] || null; - this.connected = (this.from && this.to); + this.from = this.graph.nodes[this.fromId] || null; + this.to = this.graph.nodes[this.toId] || null; + this.connected = (this.from && this.to); - if (this.connected) { - this.from.attachEdge(this); - this.to.attachEdge(this); + if (this.connected) { + this.from.attachEdge(this); + this.to.attachEdge(this); + } + else { + if (this.from) { + this.from.detachEdge(this); } - else { - if (this.from) { - this.from.detachEdge(this); - } - if (this.to) { - this.to.detachEdge(this); - } + if (this.to) { + this.to.detachEdge(this); } + } }; /** * Disconnect an edge from its nodes */ Edge.prototype.disconnect = function () { - if (this.from) { - this.from.detachEdge(this); - this.from = null; - } - if (this.to) { - this.to.detachEdge(this); - this.to = null; - } + if (this.from) { + this.from.detachEdge(this); + this.from = null; + } + if (this.to) { + this.to.detachEdge(this); + this.to = null; + } - this.connected = false; + this.connected = false; }; /** @@ -13221,7 +12700,7 @@ Edge.prototype.disconnect = function () { * has been set. */ Edge.prototype.getTitle = function() { - return this.title; + return this.title; }; @@ -13230,7 +12709,7 @@ Edge.prototype.getTitle = function() { * @return {Number} value */ Edge.prototype.getValue = function() { - return this.value; + return this.value; }; /** @@ -13240,10 +12719,10 @@ Edge.prototype.getValue = function() { * @param {Number} max */ Edge.prototype.setValueRange = function(min, max) { - if (!this.widthFixed && this.value !== undefined) { - var factor = (this.widthMax - this.widthMin) / (max - min); - this.width = (this.value - min) * factor + this.widthMin; - } + if (!this.widthFixed && this.value !== undefined) { + var scale = (this.widthMax - this.widthMin) / (max - min); + this.width = (this.value - min) * scale + this.widthMin; + } }; /** @@ -13253,7 +12732,7 @@ Edge.prototype.setValueRange = function(min, max) { * @param {CanvasRenderingContext2D} ctx */ Edge.prototype.draw = function(ctx) { - throw "Method draw not initialized in edge"; + throw "Method draw not initialized in edge"; }; /** @@ -13262,19 +12741,19 @@ Edge.prototype.draw = function(ctx) { * @return {boolean} True if location is located on the edge */ Edge.prototype.isOverlappingWith = function(obj) { - var distMax = 10; + var distMax = 10; - var xFrom = this.from.x; - var yFrom = this.from.y; - var xTo = this.to.x; - var yTo = this.to.y; - var xObj = obj.left; - var yObj = obj.top; + var xFrom = this.from.x; + var yFrom = this.from.y; + var xTo = this.to.x; + var yTo = this.to.y; + var xObj = obj.left; + var yObj = obj.top; - var dist = Edge._dist(xFrom, yFrom, xTo, yTo, xObj, yObj); + var dist = Edge._dist(xFrom, yFrom, xTo, yTo, xObj, yObj); - return (dist < distMax); + return (dist < distMax); }; @@ -13286,40 +12765,40 @@ Edge.prototype.isOverlappingWith = function(obj) { * @private */ Edge.prototype._drawLine = function(ctx) { - // set style - ctx.strokeStyle = this.color; - ctx.lineWidth = this._getLineWidth(); + // set style + ctx.strokeStyle = this.color; + ctx.lineWidth = this._getLineWidth(); - var point; - if (this.from != this.to) { - // draw line - this._line(ctx); + var point; + if (this.from != this.to) { + // draw line + this._line(ctx); - // draw label - if (this.label) { - point = this._pointOnLine(0.5); - this._label(ctx, this.label, point.x, point.y); - } + // draw label + if (this.label) { + point = this._pointOnLine(0.5); + this._label(ctx, this.label, point.x, point.y); + } + } + else { + var x, y; + var radius = this.length / 4; + var node = this.from; + if (!node.width) { + node.resize(ctx); + } + if (node.width > node.height) { + x = node.x + node.width / 2; + y = node.y - radius; } else { - var x, y; - var radius = this.length / 4; - var node = this.from; - if (!node.width) { - node.resize(ctx); - } - if (node.width > node.height) { - x = node.x + node.width / 2; - y = node.y - radius; - } - else { - x = node.x + radius; - y = node.y - node.height / 2; - } - this._circle(ctx, x, y, radius); - point = this._pointOnCircle(x, y, radius, 0.5); - this._label(ctx, this.label, point.x, point.y); + x = node.x + radius; + y = node.y - node.height / 2; } + this._circle(ctx, x, y, radius); + point = this._pointOnCircle(x, y, radius, 0.5); + this._label(ctx, this.label, point.x, point.y); + } }; /** @@ -13329,12 +12808,12 @@ Edge.prototype._drawLine = function(ctx) { * @private */ Edge.prototype._getLineWidth = function() { - if (this.from.selected || this.to.selected) { - return Math.min(this.width * 2, this.widthMax); - } - else { - return this.width; - } + if (this.from.selected || this.to.selected) { + return Math.min(this.width * 2, this.widthMax); + } + else { + return this.width; + } }; /** @@ -13343,11 +12822,11 @@ Edge.prototype._getLineWidth = function() { * @private */ Edge.prototype._line = function (ctx) { - // draw a straight line - ctx.beginPath(); - ctx.moveTo(this.from.x, this.from.y); - ctx.lineTo(this.to.x, this.to.y); - ctx.stroke(); + // draw a straight line + ctx.beginPath(); + ctx.moveTo(this.from.x, this.from.y); + ctx.lineTo(this.to.x, this.to.y); + ctx.stroke(); }; /** @@ -13359,10 +12838,10 @@ Edge.prototype._line = function (ctx) { * @private */ Edge.prototype._circle = function (ctx, x, y, radius) { - // draw a circle - ctx.beginPath(); - ctx.arc(x, y, radius, 0, 2 * Math.PI, false); - ctx.stroke(); + // draw a circle + ctx.beginPath(); + ctx.arc(x, y, radius, 0, 2 * Math.PI, false); + ctx.stroke(); }; /** @@ -13374,24 +12853,24 @@ Edge.prototype._circle = function (ctx, x, y, radius) { * @private */ Edge.prototype._label = function (ctx, text, x, y) { - if (text) { - // TODO: cache the calculated size - ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") + - this.fontSize + "px " + this.fontFace; - ctx.fillStyle = 'white'; - var width = ctx.measureText(text).width; - var height = this.fontSize; - var left = x - width / 2; - var top = y - height / 2; - - ctx.fillRect(left, top, width, height); - - // draw text - ctx.fillStyle = this.fontColor || "black"; - ctx.textAlign = "left"; - ctx.textBaseline = "top"; - ctx.fillText(text, left, top); - } + if (text) { + // TODO: cache the calculated size + ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") + + this.fontSize + "px " + this.fontFace; + ctx.fillStyle = 'white'; + var width = ctx.measureText(text).width; + var height = this.fontSize; + var left = x - width / 2; + var top = y - height / 2; + + ctx.fillRect(left, top, width, height); + + // draw text + ctx.fillStyle = this.fontColor || "black"; + ctx.textAlign = "left"; + ctx.textBaseline = "top"; + ctx.fillText(text, left, top); + } }; /** @@ -13404,35 +12883,35 @@ Edge.prototype._label = function (ctx, text, x, y) { * @private */ Edge.prototype._drawDashLine = function(ctx) { - // set style - ctx.strokeStyle = this.color; - ctx.lineWidth = this._getLineWidth(); - - // draw dashed line - ctx.beginPath(); - ctx.lineCap = 'round'; - if (this.dash.altLength != undefined) //If an alt dash value has been set add to the array this value - { - ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y, - [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]); - } - else if (this.dash.length != undefined && this.dash.gap != undefined) //If a dash and gap value has been set add to the array this value - { - ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y, - [this.dash.length,this.dash.gap]); - } - else //If all else fails draw a line - { - ctx.moveTo(this.from.x, this.from.y); - ctx.lineTo(this.to.x, this.to.y); - } - ctx.stroke(); + // set style + ctx.strokeStyle = this.color; + ctx.lineWidth = this._getLineWidth(); + + // draw dashed line + ctx.beginPath(); + ctx.lineCap = 'round'; + if (this.dash.altLength != undefined) //If an alt dash value has been set add to the array this value + { + ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y, + [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]); + } + else if (this.dash.length != undefined && this.dash.gap != undefined) //If a dash and gap value has been set add to the array this value + { + ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y, + [this.dash.length,this.dash.gap]); + } + else //If all else fails draw a line + { + ctx.moveTo(this.from.x, this.from.y); + ctx.lineTo(this.to.x, this.to.y); + } + ctx.stroke(); - // draw label - if (this.label) { - var point = this._pointOnLine(0.5); - this._label(ctx, this.label, point.x, point.y); - } + // draw label + if (this.label) { + var point = this._pointOnLine(0.5); + this._label(ctx, this.label, point.x, point.y); + } }; /** @@ -13442,10 +12921,10 @@ Edge.prototype._drawDashLine = function(ctx) { * @private */ Edge.prototype._pointOnLine = function (percentage) { - return { - x: (1 - percentage) * this.from.x + percentage * this.to.x, - y: (1 - percentage) * this.from.y + percentage * this.to.y - } + return { + x: (1 - percentage) * this.from.x + percentage * this.to.x, + y: (1 - percentage) * this.from.y + percentage * this.to.y + } }; /** @@ -13458,11 +12937,11 @@ Edge.prototype._pointOnLine = function (percentage) { * @private */ Edge.prototype._pointOnCircle = function (x, y, radius, percentage) { - var angle = (percentage - 3/8) * 2 * Math.PI; - return { - x: x + radius * Math.cos(angle), - y: y - radius * Math.sin(angle) - } + var angle = (percentage - 3/8) * 2 * Math.PI; + return { + x: x + radius * Math.cos(angle), + y: y - radius * Math.sin(angle) + } }; /** @@ -13473,62 +12952,62 @@ Edge.prototype._pointOnCircle = function (x, y, radius, percentage) { * @private */ Edge.prototype._drawArrowCenter = function(ctx) { - var point; - // set style - ctx.strokeStyle = this.color; - ctx.fillStyle = this.color; - ctx.lineWidth = this._getLineWidth(); - - if (this.from != this.to) { - // draw line - this._line(ctx); - - // draw an arrow halfway the line - var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); - var length = 10 + 5 * this.width; // TODO: make customizable? - point = this._pointOnLine(0.5); - ctx.arrow(point.x, point.y, angle, length); - ctx.fill(); - ctx.stroke(); - - // draw label - if (this.label) { - point = this._pointOnLine(0.5); - this._label(ctx, this.label, point.x, point.y); - } + var point; + // set style + ctx.strokeStyle = this.color; + ctx.fillStyle = this.color; + ctx.lineWidth = this._getLineWidth(); + + if (this.from != this.to) { + // draw line + this._line(ctx); + + // draw an arrow halfway the line + var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); + var length = 10 + 5 * this.width; // TODO: make customizable? + point = this._pointOnLine(0.5); + ctx.arrow(point.x, point.y, angle, length); + ctx.fill(); + ctx.stroke(); + + // draw label + if (this.label) { + point = this._pointOnLine(0.5); + this._label(ctx, this.label, point.x, point.y); + } + } + else { + // draw circle + var x, y; + var radius = this.length / 4; + var node = this.from; + if (!node.width) { + node.resize(ctx); + } + if (node.width > node.height) { + x = node.x + node.width / 2; + y = node.y - radius; } else { - // draw circle - var x, y; - var radius = this.length / 4; - var node = this.from; - if (!node.width) { - node.resize(ctx); - } - if (node.width > node.height) { - x = node.x + node.width / 2; - y = node.y - radius; - } - else { - x = node.x + radius; - y = node.y - node.height / 2; - } - this._circle(ctx, x, y, radius); + x = node.x + radius; + y = node.y - node.height / 2; + } + this._circle(ctx, x, y, radius); - // draw all arrows - var angle = 0.2 * Math.PI; - var length = 10 + 5 * this.width; // TODO: make customizable? - point = this._pointOnCircle(x, y, radius, 0.5); - ctx.arrow(point.x, point.y, angle, length); - ctx.fill(); - ctx.stroke(); + // draw all arrows + var angle = 0.2 * Math.PI; + var length = 10 + 5 * this.width; // TODO: make customizable? + point = this._pointOnCircle(x, y, radius, 0.5); + ctx.arrow(point.x, point.y, angle, length); + ctx.fill(); + ctx.stroke(); - // draw label - if (this.label) { - point = this._pointOnCircle(x, y, radius, 0.5); - this._label(ctx, this.label, point.x, point.y); - } + // draw label + if (this.label) { + point = this._pointOnCircle(x, y, radius, 0.5); + this._label(ctx, this.label, point.x, point.y); } + } }; @@ -13541,91 +13020,91 @@ Edge.prototype._drawArrowCenter = function(ctx) { * @private */ Edge.prototype._drawArrow = function(ctx) { - // set style - ctx.strokeStyle = this.color; - ctx.fillStyle = this.color; - ctx.lineWidth = this._getLineWidth(); + // set style + ctx.strokeStyle = this.color; + ctx.fillStyle = this.color; + ctx.lineWidth = this._getLineWidth(); + + // draw line + var angle, length; + if (this.from != this.to) { + // calculate length and angle of the line + angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); + var dx = (this.to.x - this.from.x); + var dy = (this.to.y - this.from.y); + var lEdge = Math.sqrt(dx * dx + dy * dy); + + var lFrom = this.from.distanceToBorder(ctx, angle + Math.PI); + var pFrom = (lEdge - lFrom) / lEdge; + var xFrom = (pFrom) * this.from.x + (1 - pFrom) * this.to.x; + var yFrom = (pFrom) * this.from.y + (1 - pFrom) * this.to.y; + + var lTo = this.to.distanceToBorder(ctx, angle); + var pTo = (lEdge - lTo) / lEdge; + var xTo = (1 - pTo) * this.from.x + pTo * this.to.x; + var yTo = (1 - pTo) * this.from.y + pTo * this.to.y; - // draw line - var angle, length; - if (this.from != this.to) { - // calculate length and angle of the line - angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); - var dx = (this.to.x - this.from.x); - var dy = (this.to.y - this.from.y); - var lEdge = Math.sqrt(dx * dx + dy * dy); - - var lFrom = this.from.distanceToBorder(ctx, angle + Math.PI); - var pFrom = (lEdge - lFrom) / lEdge; - var xFrom = (pFrom) * this.from.x + (1 - pFrom) * this.to.x; - var yFrom = (pFrom) * this.from.y + (1 - pFrom) * this.to.y; - - var lTo = this.to.distanceToBorder(ctx, angle); - var pTo = (lEdge - lTo) / lEdge; - var xTo = (1 - pTo) * this.from.x + pTo * this.to.x; - var yTo = (1 - pTo) * this.from.y + pTo * this.to.y; - - ctx.beginPath(); - ctx.moveTo(xFrom, yFrom); - ctx.lineTo(xTo, yTo); - ctx.stroke(); - - // draw arrow at the end of the line - length = 10 + 5 * this.width; // TODO: make customizable? - ctx.arrow(xTo, yTo, angle, length); - ctx.fill(); - ctx.stroke(); - - // draw label - if (this.label) { - var point = this._pointOnLine(0.5); - this._label(ctx, this.label, point.x, point.y); - } + ctx.beginPath(); + ctx.moveTo(xFrom, yFrom); + ctx.lineTo(xTo, yTo); + ctx.stroke(); + + // draw arrow at the end of the line + length = 10 + 5 * this.width; // TODO: make customizable? + ctx.arrow(xTo, yTo, angle, length); + ctx.fill(); + ctx.stroke(); + + // draw label + if (this.label) { + var point = this._pointOnLine(0.5); + this._label(ctx, this.label, point.x, point.y); + } + } + else { + // draw circle + var node = this.from; + var x, y, arrow; + var radius = this.length / 4; + if (!node.width) { + node.resize(ctx); + } + if (node.width > node.height) { + x = node.x + node.width / 2; + y = node.y - radius; + arrow = { + x: x, + y: node.y, + angle: 0.9 * Math.PI + }; } else { - // draw circle - var node = this.from; - var x, y, arrow; - var radius = this.length / 4; - if (!node.width) { - node.resize(ctx); - } - if (node.width > node.height) { - x = node.x + node.width / 2; - y = node.y - radius; - arrow = { - x: x, - y: node.y, - angle: 0.9 * Math.PI - }; - } - else { - x = node.x + radius; - y = node.y - node.height / 2; - arrow = { - x: node.x, - y: y, - angle: 0.6 * Math.PI - }; - } - ctx.beginPath(); - // TODO: do not draw a circle, but an arc - // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center - ctx.arc(x, y, radius, 0, 2 * Math.PI, false); - ctx.stroke(); + x = node.x + radius; + y = node.y - node.height / 2; + arrow = { + x: node.x, + y: y, + angle: 0.6 * Math.PI + }; + } + ctx.beginPath(); + // TODO: do not draw a circle, but an arc + // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center + ctx.arc(x, y, radius, 0, 2 * Math.PI, false); + ctx.stroke(); - // draw all arrows - length = 10 + 5 * this.width; // TODO: make customizable? - ctx.arrow(arrow.x, arrow.y, arrow.angle, length); - ctx.fill(); - ctx.stroke(); + // draw all arrows + length = 10 + 5 * this.width; // TODO: make customizable? + ctx.arrow(arrow.x, arrow.y, arrow.angle, length); + ctx.fill(); + ctx.stroke(); - // draw label - if (this.label) { - point = this._pointOnCircle(x, y, radius, 0.5); - this._label(ctx, this.label, point.x, point.y); - } + // draw label + if (this.label) { + point = this._pointOnCircle(x, y, radius, 0.5); + this._label(ctx, this.label, point.x, point.y); } + } }; @@ -13643,30 +13122,30 @@ Edge.prototype._drawArrow = function(ctx) { * @private */ Edge._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point - var px = x2-x1, - py = y2-y1, - something = px*px + py*py, - u = ((x3 - x1) * px + (y3 - y1) * py) / something; + var px = x2-x1, + py = y2-y1, + something = px*px + py*py, + u = ((x3 - x1) * px + (y3 - y1) * py) / something; - if (u > 1) { - u = 1; - } - else if (u < 0) { - u = 0; - } + if (u > 1) { + u = 1; + } + else if (u < 0) { + u = 0; + } - var x = x1 + u * px, - y = y1 + u * py, - dx = x - x3, - dy = y - y3; + var x = x1 + u * px, + y = y1 + u * py, + dx = x - x3, + dy = y - y3; - //# Note: If the actual distance does not matter, - //# if you only want to compare what this function - //# returns to other results of this function, you - //# can just return the squared distance instead - //# (i.e. remove the sqrt) to gain a little performance + //# Note: If the actual distance does not matter, + //# if you only want to compare what this function + //# returns to other results of this function, you + //# can just return the squared distance instead + //# (i.e. remove the sqrt) to gain a little performance - return Math.sqrt(dx*dx + dy*dy); + return Math.sqrt(dx*dx + dy*dy); }; /** @@ -13677,38 +13156,38 @@ Edge._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point * @param {String} [text] */ function Popup(container, x, y, text) { - if (container) { - this.container = container; - } - else { - this.container = document.body; - } - this.x = 0; - this.y = 0; - this.padding = 5; + if (container) { + this.container = container; + } + else { + this.container = document.body; + } + this.x = 0; + this.y = 0; + this.padding = 5; - if (x !== undefined && y !== undefined ) { - this.setPosition(x, y); - } - if (text !== undefined) { - this.setText(text); - } + if (x !== undefined && y !== undefined ) { + this.setPosition(x, y); + } + if (text !== undefined) { + this.setText(text); + } - // create the frame - this.frame = document.createElement("div"); - var style = this.frame.style; - style.position = "absolute"; - style.visibility = "hidden"; - style.border = "1px solid #666"; - style.color = "black"; - style.padding = this.padding + "px"; - style.backgroundColor = "#FFFFC6"; - style.borderRadius = "3px"; - style.MozBorderRadius = "3px"; - style.WebkitBorderRadius = "3px"; - style.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)"; - style.whiteSpace = "nowrap"; - this.container.appendChild(this.frame); + // create the frame + this.frame = document.createElement("div"); + var style = this.frame.style; + style.position = "absolute"; + style.visibility = "hidden"; + style.border = "1px solid #666"; + style.color = "black"; + style.padding = this.padding + "px"; + style.backgroundColor = "#FFFFC6"; + style.borderRadius = "3px"; + style.MozBorderRadius = "3px"; + style.WebkitBorderRadius = "3px"; + style.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)"; + style.whiteSpace = "nowrap"; + this.container.appendChild(this.frame); }; /** @@ -13716,8 +13195,8 @@ function Popup(container, x, y, text) { * @param {number} y Vertical position of the popup window */ Popup.prototype.setPosition = function(x, y) { - this.x = parseInt(x); - this.y = parseInt(y); + this.x = parseInt(x); + this.y = parseInt(y); }; /** @@ -13725,7 +13204,7 @@ Popup.prototype.setPosition = function(x, y) { * @param {string} text */ Popup.prototype.setText = function(text) { - this.frame.innerHTML = text; + this.frame.innerHTML = text; }; /** @@ -13733,46 +13212,46 @@ Popup.prototype.setText = function(text) { * @param {boolean} show Optional. Show or hide the window */ Popup.prototype.show = function (show) { - if (show === undefined) { - show = true; - } - - if (show) { - var height = this.frame.clientHeight; - var width = this.frame.clientWidth; - var maxHeight = this.frame.parentNode.clientHeight; - var maxWidth = this.frame.parentNode.clientWidth; + if (show === undefined) { + show = true; + } - var top = (this.y - height); - if (top + height + this.padding > maxHeight) { - top = maxHeight - height - this.padding; - } - if (top < this.padding) { - top = this.padding; - } + if (show) { + var height = this.frame.clientHeight; + var width = this.frame.clientWidth; + var maxHeight = this.frame.parentNode.clientHeight; + var maxWidth = this.frame.parentNode.clientWidth; - var left = this.x; - if (left + width + this.padding > maxWidth) { - left = maxWidth - width - this.padding; - } - if (left < this.padding) { - left = this.padding; - } + var top = (this.y - height); + if (top + height + this.padding > maxHeight) { + top = maxHeight - height - this.padding; + } + if (top < this.padding) { + top = this.padding; + } - this.frame.style.left = left + "px"; - this.frame.style.top = top + "px"; - this.frame.style.visibility = "visible"; + var left = this.x; + if (left + width + this.padding > maxWidth) { + left = maxWidth - width - this.padding; } - else { - this.hide(); + if (left < this.padding) { + left = this.padding; } + + this.frame.style.left = left + "px"; + this.frame.style.top = top + "px"; + this.frame.style.visibility = "visible"; + } + else { + this.hide(); + } }; /** * Hide the popup window */ Popup.prototype.hide = function () { - this.frame.style.visibility = "hidden"; + this.frame.style.visibility = "hidden"; }; /** @@ -13780,8 +13259,8 @@ Popup.prototype.hide = function () { * This class can store groups and properties specific for groups. */ Groups = function () { - this.clear(); - this.defaultIndex = 0; + this.clear(); + this.defaultIndex = 0; }; @@ -13789,16 +13268,16 @@ Groups = function () { * default constants for group colors */ Groups.DEFAULT = [ - {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue - {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}}, // yellow - {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}}, // red - {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}}, // green - {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}}, // magenta - {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}}, // purple - {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}}, // orange - {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue - {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}}, // pink - {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}} // mint + {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue + {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}}, // yellow + {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}}, // red + {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}}, // green + {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}}, // magenta + {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}}, // purple + {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}}, // orange + {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue + {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}}, // pink + {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}} // mint ]; @@ -13806,17 +13285,17 @@ Groups.DEFAULT = [ * Clear all groups */ Groups.prototype.clear = function () { - this.groups = {}; - this.groups.length = function() - { - var i = 0; - for ( var p in this ) { - if (this.hasOwnProperty(p)) { - i++; - } - } - return i; + this.groups = {}; + this.groups.length = function() + { + var i = 0; + for ( var p in this ) { + if (this.hasOwnProperty(p)) { + i++; + } } + return i; + } }; @@ -13827,18 +13306,18 @@ Groups.prototype.clear = function () { * @return {Object} group The created group, containing all group properties */ Groups.prototype.get = function (groupname) { - var group = this.groups[groupname]; - - if (group == undefined) { - // create new group - var index = this.defaultIndex % Groups.DEFAULT.length; - this.defaultIndex++; - group = {}; - group.color = Groups.DEFAULT[index]; - this.groups[groupname] = group; - } + var group = this.groups[groupname]; + + if (group == undefined) { + // create new group + var index = this.defaultIndex % Groups.DEFAULT.length; + this.defaultIndex++; + group = {}; + group.color = Groups.DEFAULT[index]; + this.groups[groupname] = group; + } - return group; + return group; }; /** @@ -13849,11 +13328,11 @@ Groups.prototype.get = function (groupname) { * @return {Object} group The created group object */ Groups.prototype.add = function (groupname, style) { - this.groups[groupname] = style; - if (style.color) { - style.color = Node.parseColor(style.color); - } - return style; + this.groups[groupname] = style; + if (style.color) { + style.color = Node.parseColor(style.color); + } + return style; }; /** @@ -13861,9 +13340,9 @@ Groups.prototype.add = function (groupname, style) { * This class loads images and keeps them stored. */ Images = function () { - this.images = {}; + this.images = {}; - this.callback = undefined; + this.callback = undefined; }; /** @@ -13872,7 +13351,7 @@ Images = function () { * @param {function} callback */ Images.prototype.setOnloadCallback = function(callback) { - this.callback = callback; + this.callback = callback; }; /** @@ -13881,27 +13360,27 @@ Images.prototype.setOnloadCallback = function(callback) { * @return {Image} img The image object */ Images.prototype.load = function(url) { - var img = this.images[url]; - if (img == undefined) { - // create the image - var images = this; - img = new Image(); - this.images[url] = img; - img.onload = function() { - if (images.callback) { - images.callback(this); - } - }; - img.src = url; - } + var img = this.images[url]; + if (img == undefined) { + // create the image + var images = this; + img = new Image(); + this.images[url] = img; + img.onload = function() { + if (images.callback) { + images.callback(this); + } + }; + img.src = url; + } - return img; + return img; }; /** * @constructor Graph * Create a graph visualization, displaying nodes and edges. - * + * * @param {Element} container The DOM element in which the Graph will * be created. Normally a div element. * @param {Object} data An object containing parameters @@ -13910,125 +13389,125 @@ Images.prototype.load = function(url) { * @param {Object} options Options */ function Graph (container, data, options) { - // create variables and set default values - this.containerElement = container; - this.width = '100%'; - this.height = '100%'; - this.refreshRate = 50; // milliseconds - this.stabilize = true; // stabilize before displaying the graph - this.selectable = true; - - // set constant values - this.constants = { - nodes: { - radiusMin: 5, - radiusMax: 20, - radius: 5, - distance: 100, // px - shape: 'ellipse', - image: undefined, - widthMin: 16, // px - widthMax: 64, // px - fontColor: 'black', - fontSize: 14, // px - //fontFace: verdana, - fontFace: 'arial', - color: { - border: '#2B7CE9', - background: '#97C2FC', - highlight: { - border: '#2B7CE9', - background: '#D2E5FF' - } - }, - borderColor: '#2B7CE9', - backgroundColor: '#97C2FC', - highlightColor: '#D2E5FF', - group: undefined - }, - edges: { - widthMin: 1, - widthMax: 15, - width: 1, - style: 'line', - color: '#343434', - fontColor: '#343434', - fontSize: 14, // px - fontFace: 'arial', - //distance: 100, //px - length: 100, // px - dash: { - length: 10, - gap: 5, - altLength: undefined - } - }, - minForce: 0.05, - minVelocity: 0.02, // px/s - maxIterations: 1000 // maximum number of iteration to stabilize - }; - - var graph = this; - this.nodes = {}; // object with Node objects - this.edges = {}; // object with Edge objects - // TODO: create a counter to keep track on the number of nodes having values - // TODO: create a counter to keep track on the number of nodes currently moving - // TODO: create a counter to keep track on the number of edges having values - - this.nodesData = null; // A DataSet or DataView - this.edgesData = null; // A DataSet or DataView - - // create event listeners used to subscribe on the DataSets of the nodes and edges - var me = this; - this.nodesListeners = { - 'add': function (event, params) { - me._addNodes(params.items); - me.start(); - }, - 'update': function (event, params) { - me._updateNodes(params.items); - me.start(); - }, - 'remove': function (event, params) { - me._removeNodes(params.items); - me.start(); - } - }; - this.edgesListeners = { - 'add': function (event, params) { - me._addEdges(params.items); - me.start(); - }, - 'update': function (event, params) { - me._updateEdges(params.items); - me.start(); - }, - 'remove': function (event, params) { - me._removeEdges(params.items); - me.start(); - } - }; + // create variables and set default values + this.containerElement = container; + this.width = '100%'; + this.height = '100%'; + this.refreshRate = 50; // milliseconds + this.stabilize = true; // stabilize before displaying the graph + this.selectable = true; + + // set constant values + this.constants = { + nodes: { + radiusMin: 5, + radiusMax: 20, + radius: 5, + distance: 100, // px + shape: 'ellipse', + image: undefined, + widthMin: 16, // px + widthMax: 64, // px + fontColor: 'black', + fontSize: 14, // px + //fontFace: verdana, + fontFace: 'arial', + color: { + border: '#2B7CE9', + background: '#97C2FC', + highlight: { + border: '#2B7CE9', + background: '#D2E5FF' + } + }, + borderColor: '#2B7CE9', + backgroundColor: '#97C2FC', + highlightColor: '#D2E5FF', + group: undefined + }, + edges: { + widthMin: 1, + widthMax: 15, + width: 1, + style: 'line', + color: '#343434', + fontColor: '#343434', + fontSize: 14, // px + fontFace: 'arial', + //distance: 100, //px + length: 100, // px + dash: { + length: 10, + gap: 5, + altLength: undefined + } + }, + minForce: 0.05, + minVelocity: 0.02, // px/s + maxIterations: 1000 // maximum number of iteration to stabilize + }; + + var graph = this; + this.nodes = {}; // object with Node objects + this.edges = {}; // object with Edge objects + // TODO: create a counter to keep track on the number of nodes having values + // TODO: create a counter to keep track on the number of nodes currently moving + // TODO: create a counter to keep track on the number of edges having values + + this.nodesData = null; // A DataSet or DataView + this.edgesData = null; // A DataSet or DataView + + // create event listeners used to subscribe on the DataSets of the nodes and edges + var me = this; + this.nodesListeners = { + 'add': function (event, params) { + me._addNodes(params.items); + me.start(); + }, + 'update': function (event, params) { + me._updateNodes(params.items); + me.start(); + }, + 'remove': function (event, params) { + me._removeNodes(params.items); + me.start(); + } + }; + this.edgesListeners = { + 'add': function (event, params) { + me._addEdges(params.items); + me.start(); + }, + 'update': function (event, params) { + me._updateEdges(params.items); + me.start(); + }, + 'remove': function (event, params) { + me._removeEdges(params.items); + me.start(); + } + }; - this.groups = new Groups(); // object with groups - this.images = new Images(); // object with images - this.images.setOnloadCallback(function () { - graph._redraw(); - }); + this.groups = new Groups(); // object with groups + this.images = new Images(); // object with images + this.images.setOnloadCallback(function () { + graph._redraw(); + }); - // properties of the data - this.moving = false; // True if any of the nodes have an undefined position + // properties of the data + this.moving = false; // True if any of the nodes have an undefined position - this.selection = []; - this.timer = undefined; + this.selection = []; + this.timer = undefined; - // create a frame and canvas - this._create(); + // create a frame and canvas + this._create(); - // apply options - this.setOptions(options); + // apply options + this.setOptions(options); - // draw data - this.setData(data); + // draw data + this.setData(data); } /** @@ -14041,33 +13520,33 @@ function Graph (container, data, options) { * {Options} [options] Object with options */ Graph.prototype.setData = function(data) { - if (data && data.dot && (data.nodes || data.edges)) { - throw new SyntaxError('Data must contain either parameter "dot" or ' + - ' parameter pair "nodes" and "edges", but not both.'); - } + if (data && data.dot && (data.nodes || data.edges)) { + throw new SyntaxError('Data must contain either parameter "dot" or ' + + ' parameter pair "nodes" and "edges", but not both.'); + } - // set options - this.setOptions(data && data.options); + // set options + this.setOptions(data && data.options); - // set all data - if (data && data.dot) { - // parse DOT file - if(data && data.dot) { - var dotData = vis.util.DOTToGraph(data.dot); - this.setData(dotData); - return; - } - } - else { - this._setNodes(data && data.nodes); - this._setEdges(data && data.edges); + // set all data + if (data && data.dot) { + // parse DOT file + if(data && data.dot) { + var dotData = vis.util.DOTToGraph(data.dot); + this.setData(dotData); + return; } + } + else { + this._setNodes(data && data.nodes); + this._setEdges(data && data.edges); + } - // find a stable position or start animating to a stable position - if (this.stabilize) { - this._doStabilize(); - } - this.start(); + // find a stable position or start animating to a stable position + if (this.stabilize) { + this._doStabilize(); + } + this.start(); }; /** @@ -14075,77 +13554,77 @@ Graph.prototype.setData = function(data) { * @param {Object} options */ Graph.prototype.setOptions = function (options) { - if (options) { - // retrieve parameter values - if (options.width != undefined) {this.width = options.width;} - if (options.height != undefined) {this.height = options.height;} - if (options.stabilize != undefined) {this.stabilize = options.stabilize;} - if (options.selectable != undefined) {this.selectable = options.selectable;} - - // TODO: work out these options and document them - if (options.edges) { - for (var prop in options.edges) { - if (options.edges.hasOwnProperty(prop)) { - this.constants.edges[prop] = options.edges[prop]; - } - } + if (options) { + // retrieve parameter values + if (options.width != undefined) {this.width = options.width;} + if (options.height != undefined) {this.height = options.height;} + if (options.stabilize != undefined) {this.stabilize = options.stabilize;} + if (options.selectable != undefined) {this.selectable = options.selectable;} + + // TODO: work out these options and document them + if (options.edges) { + for (var prop in options.edges) { + if (options.edges.hasOwnProperty(prop)) { + this.constants.edges[prop] = options.edges[prop]; + } + } - if (options.edges.length != undefined && - options.nodes && options.nodes.distance == undefined) { - this.constants.edges.length = options.edges.length; - this.constants.nodes.distance = options.edges.length * 1.25; - } + if (options.edges.length != undefined && + options.nodes && options.nodes.distance == undefined) { + this.constants.edges.length = options.edges.length; + this.constants.nodes.distance = options.edges.length * 1.25; + } - if (!options.edges.fontColor) { - this.constants.edges.fontColor = options.edges.color; - } + if (!options.edges.fontColor) { + this.constants.edges.fontColor = options.edges.color; + } - // Added to support dashed lines - // David Jordan - // 2012-08-08 - if (options.edges.dash) { - if (options.edges.dash.length != undefined) { - this.constants.edges.dash.length = options.edges.dash.length; - } - if (options.edges.dash.gap != undefined) { - this.constants.edges.dash.gap = options.edges.dash.gap; - } - if (options.edges.dash.altLength != undefined) { - this.constants.edges.dash.altLength = options.edges.dash.altLength; - } - } + // Added to support dashed lines + // David Jordan + // 2012-08-08 + if (options.edges.dash) { + if (options.edges.dash.length != undefined) { + this.constants.edges.dash.length = options.edges.dash.length; + } + if (options.edges.dash.gap != undefined) { + this.constants.edges.dash.gap = options.edges.dash.gap; } + if (options.edges.dash.altLength != undefined) { + this.constants.edges.dash.altLength = options.edges.dash.altLength; + } + } + } - if (options.nodes) { - for (prop in options.nodes) { - if (options.nodes.hasOwnProperty(prop)) { - this.constants.nodes[prop] = options.nodes[prop]; - } - } + if (options.nodes) { + for (prop in options.nodes) { + if (options.nodes.hasOwnProperty(prop)) { + this.constants.nodes[prop] = options.nodes[prop]; + } + } - if (options.nodes.color) { - this.constants.nodes.color = Node.parseColor(options.nodes.color); - } + if (options.nodes.color) { + this.constants.nodes.color = Node.parseColor(options.nodes.color); + } - /* - if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin; - if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax; - */ - } + /* + if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin; + if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax; + */ + } - if (options.groups) { - for (var groupname in options.groups) { - if (options.groups.hasOwnProperty(groupname)) { - var group = options.groups[groupname]; - this.groups.add(groupname, group); - } - } + if (options.groups) { + for (var groupname in options.groups) { + if (options.groups.hasOwnProperty(groupname)) { + var group = options.groups[groupname]; + this.groups.add(groupname, group); } + } } + } - this.setSize(this.width, this.height); - this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2); - this._setScale(1); + this.setSize(this.width, this.height); + this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2); + this._setScale(1); }; /** @@ -14155,7 +13634,7 @@ Graph.prototype.setOptions = function (options) { * @private */ Graph.prototype._trigger = function (event, params) { - events.trigger(this, event, params); + events.trigger(this, event, params); }; @@ -14167,48 +13646,48 @@ Graph.prototype._trigger = function (event, params) { * @private */ Graph.prototype._create = function () { - // remove all elements from the container element. - while (this.containerElement.hasChildNodes()) { - this.containerElement.removeChild(this.containerElement.firstChild); - } - - this.frame = document.createElement('div'); - this.frame.className = 'graph-frame'; - this.frame.style.position = 'relative'; - this.frame.style.overflow = 'hidden'; + // remove all elements from the container element. + while (this.containerElement.hasChildNodes()) { + this.containerElement.removeChild(this.containerElement.firstChild); + } - // create the graph canvas (HTML canvas element) - this.frame.canvas = document.createElement( 'canvas' ); - this.frame.canvas.style.position = 'relative'; - this.frame.appendChild(this.frame.canvas); - if (!this.frame.canvas.getContext) { - var noCanvas = document.createElement( 'DIV' ); - noCanvas.style.color = 'red'; - noCanvas.style.fontWeight = 'bold' ; - noCanvas.style.padding = '10px'; - noCanvas.innerHTML = 'Error: your browser does not support HTML canvas'; - this.frame.canvas.appendChild(noCanvas); - } + this.frame = document.createElement('div'); + this.frame.className = 'graph-frame'; + this.frame.style.position = 'relative'; + this.frame.style.overflow = 'hidden'; + + // create the graph canvas (HTML canvas element) + this.frame.canvas = document.createElement( 'canvas' ); + this.frame.canvas.style.position = 'relative'; + this.frame.appendChild(this.frame.canvas); + if (!this.frame.canvas.getContext) { + var noCanvas = document.createElement( 'DIV' ); + noCanvas.style.color = 'red'; + noCanvas.style.fontWeight = 'bold' ; + noCanvas.style.padding = '10px'; + noCanvas.innerHTML = 'Error: your browser does not support HTML canvas'; + this.frame.canvas.appendChild(noCanvas); + } - var me = this; - this.drag = {}; - this.pinch = {}; - this.hammer = Hammer(this.frame.canvas, { - prevent_default: true - }); - this.hammer.on('tap', me._onTap.bind(me) ); - this.hammer.on('hold', me._onHold.bind(me) ); - this.hammer.on('pinch', me._onPinch.bind(me) ); - this.hammer.on('touch', me._onTouch.bind(me) ); - this.hammer.on('dragstart', me._onDragStart.bind(me) ); - this.hammer.on('drag', me._onDrag.bind(me) ); - this.hammer.on('dragend', me._onDragEnd.bind(me) ); - this.hammer.on('mousewheel',me._onMouseWheel.bind(me) ); - this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF - this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) ); + var me = this; + this.drag = {}; + this.pinch = {}; + this.hammer = Hammer(this.frame.canvas, { + prevent_default: true + }); + this.hammer.on('tap', me._onTap.bind(me) ); + this.hammer.on('hold', me._onHold.bind(me) ); + this.hammer.on('pinch', me._onPinch.bind(me) ); + this.hammer.on('touch', me._onTouch.bind(me) ); + this.hammer.on('dragstart', me._onDragStart.bind(me) ); + this.hammer.on('drag', me._onDrag.bind(me) ); + this.hammer.on('dragend', me._onDragEnd.bind(me) ); + this.hammer.on('mousewheel',me._onMouseWheel.bind(me) ); + this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF + this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) ); - // add the frame to the container element - this.containerElement.appendChild(this.frame); + // add the frame to the container element + this.containerElement.appendChild(this.frame); }; /** @@ -14218,21 +13697,21 @@ Graph.prototype._create = function () { * @private */ Graph.prototype._getNodeAt = function (pointer) { - var x = this._canvasToX(pointer.x); - var y = this._canvasToY(pointer.y); - - var obj = { - left: x, - top: y, - right: x, - bottom: y - }; + var x = this._canvasToX(pointer.x); + var y = this._canvasToY(pointer.y); - // if there are overlapping nodes, select the last one, this is the - // one which is drawn on top of the others - var overlappingNodes = this._getNodesOverlappingWith(obj); - return (overlappingNodes.length > 0) ? - overlappingNodes[overlappingNodes.length - 1] : null; + var obj = { + left: x, + top: y, + right: x, + bottom: y + }; + + // if there are overlapping nodes, select the last one, this is the + // one which is drawn on top of the others + var overlappingNodes = this._getNodesOverlappingWith(obj); + return (overlappingNodes.length > 0) ? + overlappingNodes[overlappingNodes.length - 1] : null; }; /** @@ -14242,10 +13721,10 @@ Graph.prototype._getNodeAt = function (pointer) { * @private */ Graph.prototype._getPointer = function (touch) { - return { - x: touch.pageX - vis.util.getAbsoluteLeft(this.frame.canvas), - y: touch.pageY - vis.util.getAbsoluteTop(this.frame.canvas) - }; + return { + x: touch.pageX - vis.util.getAbsoluteLeft(this.frame.canvas), + y: touch.pageY - vis.util.getAbsoluteTop(this.frame.canvas) + }; }; /** @@ -14254,9 +13733,9 @@ Graph.prototype._getPointer = function (touch) { * @private */ Graph.prototype._onTouch = function (event) { - this.drag.pointer = this._getPointer(event.gesture.touches[0]); - this.drag.pinched = false; - this.pinch.scale = this._getScale(); + this.drag.pointer = this._getPointer(event.gesture.touches[0]); + this.drag.pinched = false; + this.pinch.scale = this._getScale(); }; /** @@ -14264,44 +13743,44 @@ Graph.prototype._onTouch = function (event) { * @private */ Graph.prototype._onDragStart = function () { - var drag = this.drag; - - drag.selection = []; - drag.translation = this._getTranslation(); - drag.nodeId = this._getNodeAt(drag.pointer); - // note: drag.pointer is set in _onTouch to get the initial touch location + var drag = this.drag; - var node = this.nodes[drag.nodeId]; - if (node) { - // select the clicked node if not yet selected - if (!node.isSelected()) { - this._selectNodes([drag.nodeId]); - } - - // create an array with the selected nodes and their original location and status - var me = this; - this.selection.forEach(function (id) { - var node = me.nodes[id]; - if (node) { - var s = { - id: id, - node: node, - - // store original x, y, xFixed and yFixed, make the node temporarily Fixed - x: node.x, - y: node.y, - xFixed: node.xFixed, - yFixed: node.yFixed - }; - - node.xFixed = true; - node.yFixed = true; - - drag.selection.push(s); - } - }); + drag.selection = []; + drag.translation = this._getTranslation(); + drag.nodeId = this._getNodeAt(drag.pointer); + // note: drag.pointer is set in _onTouch to get the initial touch location + var node = this.nodes[drag.nodeId]; + if (node) { + // select the clicked node if not yet selected + if (!node.isSelected()) { + this._selectNodes([drag.nodeId]); } + + // create an array with the selected nodes and their original location and status + var me = this; + this.selection.forEach(function (id) { + var node = me.nodes[id]; + if (node) { + var s = { + id: id, + node: node, + + // store original x, y, xFixed and yFixed, make the node temporarily Fixed + x: node.x, + y: node.y, + xFixed: node.xFixed, + yFixed: node.yFixed + }; + + node.xFixed = true; + node.yFixed = true; + + drag.selection.push(s); + } + }); + + } }; /** @@ -14309,51 +13788,51 @@ Graph.prototype._onDragStart = function () { * @private */ Graph.prototype._onDrag = function (event) { - if (this.drag.pinched) { - return; - } + if (this.drag.pinched) { + return; + } - var pointer = this._getPointer(event.gesture.touches[0]); + var pointer = this._getPointer(event.gesture.touches[0]); - var me = this, - drag = this.drag, - selection = drag.selection; - if (selection && selection.length) { - // calculate delta's and new location - var deltaX = pointer.x - drag.pointer.x, - deltaY = pointer.y - drag.pointer.y; + var me = this, + drag = this.drag, + selection = drag.selection; + if (selection && selection.length) { + // calculate delta's and new location + var deltaX = pointer.x - drag.pointer.x, + deltaY = pointer.y - drag.pointer.y; - // update position of all selected nodes - selection.forEach(function (s) { - var node = s.node; + // update position of all selected nodes + selection.forEach(function (s) { + var node = s.node; - if (!s.xFixed) { - node.x = me._canvasToX(me._xToCanvas(s.x) + deltaX); - } + if (!s.xFixed) { + node.x = me._canvasToX(me._xToCanvas(s.x) + deltaX); + } - if (!s.yFixed) { - node.y = me._canvasToY(me._yToCanvas(s.y) + deltaY); - } - }); + if (!s.yFixed) { + node.y = me._canvasToY(me._yToCanvas(s.y) + deltaY); + } + }); - // start animation if not yet running - if (!this.moving) { - this.moving = true; - this.start(); - } + // start animation if not yet running + if (!this.moving) { + this.moving = true; + this.start(); } - else { - // move the graph - var diffX = pointer.x - this.drag.pointer.x; - var diffY = pointer.y - this.drag.pointer.y; - - this._setTranslation( - this.drag.translation.x + diffX, - this.drag.translation.y + diffY); - this._redraw(); + } + else { + // move the graph + var diffX = pointer.x - this.drag.pointer.x; + var diffY = pointer.y - this.drag.pointer.y; + + this._setTranslation( + this.drag.translation.x + diffX, + this.drag.translation.y + diffY); + this._redraw(); - this.moved = true; - } + this.moved = true; + } }; /** @@ -14361,14 +13840,14 @@ Graph.prototype._onDrag = function (event) { * @private */ Graph.prototype._onDragEnd = function () { - var selection = this.drag.selection; - if (selection) { - selection.forEach(function (s) { - // restore original xFixed and yFixed - s.node.xFixed = s.xFixed; - s.node.yFixed = s.yFixed; - }); - } + var selection = this.drag.selection; + if (selection) { + selection.forEach(function (s) { + // restore original xFixed and yFixed + s.node.xFixed = s.xFixed; + s.node.yFixed = s.yFixed; + }); + } }; /** @@ -14376,23 +13855,23 @@ Graph.prototype._onDragEnd = function () { * @private */ Graph.prototype._onTap = function (event) { - var pointer = this._getPointer(event.gesture.touches[0]); + var pointer = this._getPointer(event.gesture.touches[0]); - var nodeId = this._getNodeAt(pointer); - var node = this.nodes[nodeId]; - if (node) { - // select this node - this._selectNodes([nodeId]); + var nodeId = this._getNodeAt(pointer); + var node = this.nodes[nodeId]; + if (node) { + // select this node + this._selectNodes([nodeId]); - if (!this.moving) { - this._redraw(); - } - } - else { - // remove selection - this._unselectNodes(); - this._redraw(); + if (!this.moving) { + this._redraw(); } + } + else { + // remove selection + this._unselectNodes(); + this._redraw(); + } }; /** @@ -14400,26 +13879,26 @@ Graph.prototype._onTap = function (event) { * @private */ Graph.prototype._onHold = function (event) { - var pointer = this._getPointer(event.gesture.touches[0]); - var nodeId = this._getNodeAt(pointer); - var node = this.nodes[nodeId]; - if (node) { - if (!node.isSelected()) { - // select this node, keep previous selection - var append = true; - this._selectNodes([nodeId], append); - } - else { - this._unselectNodes([nodeId]); - } - - if (!this.moving) { - this._redraw(); - } + var pointer = this._getPointer(event.gesture.touches[0]); + var nodeId = this._getNodeAt(pointer); + var node = this.nodes[nodeId]; + if (node) { + if (!node.isSelected()) { + // select this node, keep previous selection + var append = true; + this._selectNodes([nodeId], append); } else { - // Do nothing + this._unselectNodes([nodeId]); + } + + if (!this.moving) { + this._redraw(); } + } + else { + // Do nothing + } }; /** @@ -14428,16 +13907,16 @@ Graph.prototype._onHold = function (event) { * @private */ Graph.prototype._onPinch = function (event) { - var pointer = this._getPointer(event.gesture.center); + var pointer = this._getPointer(event.gesture.center); - this.drag.pinched = true; - if (!('scale' in this.pinch)) { - this.pinch.scale = 1; - } + this.drag.pinched = true; + if (!('scale' in this.pinch)) { + this.pinch.scale = 1; + } - // TODO: enable moving while pinching? - var scale = this.pinch.scale * event.gesture.scale; - this._zoom(scale, pointer) + // TODO: enable moving while pinching? + var scale = this.pinch.scale * event.gesture.scale; + this._zoom(scale, pointer) }; /** @@ -14448,24 +13927,24 @@ Graph.prototype._onPinch = function (event) { * @private */ Graph.prototype._zoom = function(scale, pointer) { - var scaleOld = this._getScale(); - if (scale < 0.01) { - scale = 0.01; - } - if (scale > 10) { - scale = 10; - } + var scaleOld = this._getScale(); + if (scale < 0.01) { + scale = 0.01; + } + if (scale > 10) { + scale = 10; + } - var translation = this._getTranslation(); - var scaleFrac = scale / scaleOld; - var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac; - var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac; + var translation = this._getTranslation(); + var scaleFrac = scale / scaleOld; + var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac; + var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac; - this._setScale(scale); - this._setTranslation(tx, ty); - this._redraw(); + this._setScale(scale); + this._setTranslation(tx, ty); + this._redraw(); - return scale; + return scale; }; /** @@ -14476,45 +13955,45 @@ Graph.prototype._zoom = function(scale, pointer) { * @private */ Graph.prototype._onMouseWheel = function(event) { - // retrieve delta - var 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; - } + // retrieve delta + var 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) { - if (!('mouswheelScale' in this.pinch)) { - this.pinch.mouswheelScale = 1; - } + // 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) { + if (!('mouswheelScale' in this.pinch)) { + this.pinch.mouswheelScale = 1; + } - // calculate the new scale - var scale = this.pinch.mouswheelScale; - var zoom = delta / 10; - if (delta < 0) { - zoom = zoom / (1 - zoom); - } - scale *= (1 + zoom); + // calculate the new scale + var scale = this.pinch.mouswheelScale; + var zoom = delta / 10; + if (delta < 0) { + zoom = zoom / (1 - zoom); + } + scale *= (1 + zoom); - // calculate the pointer location - var gesture = Hammer.event.collectEventData(this, 'scroll', event); - var pointer = this._getPointer(gesture.center); + // calculate the pointer location + var gesture = Hammer.event.collectEventData(this, 'scroll', event); + var pointer = this._getPointer(gesture.center); - // apply the new scale - scale = this._zoom(scale, pointer); + // apply the new scale + scale = this._zoom(scale, pointer); - // store the new, applied scale - this.pinch.mouswheelScale = scale; - } + // store the new, applied scale + this.pinch.mouswheelScale = scale; + } - // Prevent default actions caused by mouse wheel. - event.preventDefault(); + // Prevent default actions caused by mouse wheel. + event.preventDefault(); }; @@ -14524,26 +14003,26 @@ Graph.prototype._onMouseWheel = function(event) { * @private */ Graph.prototype._onMouseMoveTitle = function (event) { - var gesture = Hammer.event.collectEventData(this, 'mousemove', event); - var pointer = this._getPointer(gesture.center); + var gesture = Hammer.event.collectEventData(this, 'mousemove', event); + var pointer = this._getPointer(gesture.center); - // check if the previously selected node is still selected - if (this.popupNode) { - this._checkHidePopup(pointer); - } + // check if the previously selected node is still selected + if (this.popupNode) { + this._checkHidePopup(pointer); + } - // start a timeout that will check if the mouse is positioned above - // an element - var me = this; - var checkShow = function() { - me._checkShowPopup(pointer); - }; - if (this.popupTimer) { - clearInterval(this.popupTimer); // stop any running timer - } - if (!this.leftButtonDown) { - this.popupTimer = setTimeout(checkShow, 300); - } + // start a timeout that will check if the mouse is positioned above + // an element + var me = this; + var checkShow = function() { + me._checkShowPopup(pointer); + }; + if (this.popupTimer) { + clearInterval(this.popupTimer); // stop any running timer + } + if (!this.leftButtonDown) { + this.popupTimer = setTimeout(checkShow, 300); + } }; /** @@ -14555,66 +14034,66 @@ Graph.prototype._onMouseMoveTitle = function (event) { * @private */ Graph.prototype._checkShowPopup = function (pointer) { - var obj = { - left: this._canvasToX(pointer.x), - top: this._canvasToY(pointer.y), - right: this._canvasToX(pointer.x), - bottom: this._canvasToY(pointer.y) - }; - - var id; - var lastPopupNode = this.popupNode; - - if (this.popupNode == undefined) { - // search the nodes for overlap, select the top one in case of multiple nodes - var nodes = this.nodes; - for (id in nodes) { - if (nodes.hasOwnProperty(id)) { - var node = nodes[id]; - if (node.getTitle() != undefined && node.isOverlappingWith(obj)) { - this.popupNode = node; - break; - } - } + var obj = { + left: this._canvasToX(pointer.x), + top: this._canvasToY(pointer.y), + right: this._canvasToX(pointer.x), + bottom: this._canvasToY(pointer.y) + }; + + var id; + var lastPopupNode = this.popupNode; + + if (this.popupNode == undefined) { + // search the nodes for overlap, select the top one in case of multiple nodes + var nodes = this.nodes; + for (id in nodes) { + if (nodes.hasOwnProperty(id)) { + var node = nodes[id]; + if (node.getTitle() != undefined && node.isOverlappingWith(obj)) { + this.popupNode = node; + break; } + } } + } - if (this.popupNode == undefined) { - // search the edges for overlap - var edges = this.edges; - for (id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; - if (edge.connected && (edge.getTitle() != undefined) && - edge.isOverlappingWith(obj)) { - this.popupNode = edge; - break; - } - } + if (this.popupNode == undefined) { + // search the edges for overlap + var edges = this.edges; + for (id in edges) { + if (edges.hasOwnProperty(id)) { + var edge = edges[id]; + if (edge.connected && (edge.getTitle() != undefined) && + edge.isOverlappingWith(obj)) { + this.popupNode = edge; + break; } + } } + } - if (this.popupNode) { - // show popup message window - if (this.popupNode != lastPopupNode) { - var me = this; - if (!me.popup) { - me.popup = new Popup(me.frame); - } + if (this.popupNode) { + // show popup message window + if (this.popupNode != lastPopupNode) { + var me = this; + if (!me.popup) { + me.popup = new Popup(me.frame); + } - // adjust a small offset such that the mouse cursor is located in the - // bottom left location of the popup, and you can easily move over the - // popup area - me.popup.setPosition(pointer.x - 3, pointer.y - 3); - me.popup.setText(me.popupNode.getTitle()); - me.popup.show(); - } + // adjust a small offset such that the mouse cursor is located in the + // bottom left location of the popup, and you can easily move over the + // popup area + me.popup.setPosition(pointer.x - 3, pointer.y - 3); + me.popup.setText(me.popupNode.getTitle()); + me.popup.show(); } - else { - if (this.popup) { - this.popup.hide(); - } + } + else { + if (this.popup) { + this.popup.hide(); } + } }; /** @@ -14624,12 +14103,12 @@ Graph.prototype._checkShowPopup = function (pointer) { * @private */ Graph.prototype._checkHidePopup = function (pointer) { - if (!this.popupNode || !this._getNodeAt(pointer) ) { - this.popupNode = undefined; - if (this.popup) { - this.popup.hide(); - } + if (!this.popupNode || !this._getNodeAt(pointer) ) { + this.popupNode = undefined; + if (this.popup) { + this.popup.hide(); } + } }; /** @@ -14643,43 +14122,43 @@ Graph.prototype._checkHidePopup = function (pointer) { * @private */ Graph.prototype._unselectNodes = function(selection, triggerSelect) { - var changed = false; - var i, iMax, id; - - if (selection) { - // remove provided selections - for (i = 0, iMax = selection.length; i < iMax; i++) { - id = selection[i]; - this.nodes[id].unselect(); - - var j = 0; - while (j < this.selection.length) { - if (this.selection[j] == id) { - this.selection.splice(j, 1); - changed = true; - } - else { - j++; - } - } + var changed = false; + var i, iMax, id; + + if (selection) { + // remove provided selections + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; + this.nodes[id].unselect(); + + var j = 0; + while (j < this.selection.length) { + if (this.selection[j] == id) { + this.selection.splice(j, 1); + changed = true; } - } - else if (this.selection && this.selection.length) { - // remove all selections - for (i = 0, iMax = this.selection.length; i < iMax; i++) { - id = this.selection[i]; - this.nodes[id].unselect(); - changed = true; + else { + j++; } - this.selection = []; + } } - - if (changed && (triggerSelect == true || triggerSelect == undefined)) { - // fire the select event - this._trigger('select'); + } + else if (this.selection && this.selection.length) { + // remove all selections + for (i = 0, iMax = this.selection.length; i < iMax; i++) { + id = this.selection[i]; + this.nodes[id].unselect(); + changed = true; } + this.selection = []; + } - return changed; + if (changed && (triggerSelect == true || triggerSelect == undefined)) { + // fire the select event + this._trigger('select'); + } + + return changed; }; /** @@ -14691,51 +14170,51 @@ Graph.prototype._unselectNodes = function(selection, triggerSelect) { * @private */ Graph.prototype._selectNodes = function(selection, append) { - var changed = false; - var i, iMax; + var changed = false; + var i, iMax; - // TODO: the selectNodes method is a little messy, rework this + // TODO: the selectNodes method is a little messy, rework this - // check if the current selection equals the desired selection - var selectionAlreadyThere = true; - if (selection.length != this.selection.length) { + // check if the current selection equals the desired selection + var selectionAlreadyThere = true; + if (selection.length != this.selection.length) { + selectionAlreadyThere = false; + } + else { + for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) { + if (selection[i] != this.selection[i]) { selectionAlreadyThere = false; + break; + } } - else { - for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) { - if (selection[i] != this.selection[i]) { - selectionAlreadyThere = false; - break; - } - } - } - if (selectionAlreadyThere) { - return changed; - } + } + if (selectionAlreadyThere) { + return changed; + } - if (append == undefined || append == false) { - // first deselect any selected node - var triggerSelect = false; - changed = this._unselectNodes(undefined, triggerSelect); - } + if (append == undefined || append == false) { + // first deselect any selected node + var triggerSelect = false; + changed = this._unselectNodes(undefined, triggerSelect); + } - for (i = 0, iMax = selection.length; i < iMax; i++) { - // add each of the new selections, but only when they are not duplicate - var id = selection[i]; - var isDuplicate = (this.selection.indexOf(id) != -1); - if (!isDuplicate) { - this.nodes[id].select(); - this.selection.push(id); - changed = true; - } + for (i = 0, iMax = selection.length; i < iMax; i++) { + // add each of the new selections, but only when they are not duplicate + var id = selection[i]; + var isDuplicate = (this.selection.indexOf(id) != -1); + if (!isDuplicate) { + this.nodes[id].select(); + this.selection.push(id); + changed = true; } + } - if (changed) { - // fire the select event - this._trigger('select'); - } + if (changed) { + // fire the select event + this._trigger('select'); + } - return changed; + return changed; }; /** @@ -14745,18 +14224,18 @@ Graph.prototype._selectNodes = function(selection, append) { * @private */ Graph.prototype._getNodesOverlappingWith = function (obj) { - var nodes = this.nodes, - overlappingNodes = []; + var nodes = this.nodes, + overlappingNodes = []; - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - if (nodes[id].isOverlappingWith(obj)) { - overlappingNodes.push(id); - } - } + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + if (nodes[id].isOverlappingWith(obj)) { + overlappingNodes.push(id); + } } + } - return overlappingNodes; + return overlappingNodes; }; /** @@ -14765,7 +14244,7 @@ Graph.prototype._getNodesOverlappingWith = function (obj) { * selected nodes. */ Graph.prototype.getSelection = function() { - return this.selection.concat([]); + return this.selection.concat([]); }; /** @@ -14774,31 +14253,31 @@ Graph.prototype.getSelection = function() { * selected nodes. */ Graph.prototype.setSelection = function(selection) { - var i, iMax, id; + var i, iMax, id; - if (!selection || (selection.length == undefined)) - throw 'Selection must be an array with ids'; + if (!selection || (selection.length == undefined)) + throw 'Selection must be an array with ids'; - // first unselect any selected node - for (i = 0, iMax = this.selection.length; i < iMax; i++) { - id = this.selection[i]; - this.nodes[id].unselect(); - } + // first unselect any selected node + for (i = 0, iMax = this.selection.length; i < iMax; i++) { + id = this.selection[i]; + this.nodes[id].unselect(); + } - this.selection = []; + this.selection = []; - for (i = 0, iMax = selection.length; i < iMax; i++) { - id = selection[i]; + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; - var node = this.nodes[id]; - if (!node) { - throw new RangeError('Node with id "' + id + '" not found'); - } - node.select(); - this.selection.push(id); + var node = this.nodes[id]; + if (!node) { + throw new RangeError('Node with id "' + id + '" not found'); } + node.select(); + this.selection.push(id); + } - this.redraw(); + this.redraw(); }; /** @@ -14806,16 +14285,16 @@ Graph.prototype.setSelection = function(selection) { * @private */ Graph.prototype._updateSelection = function () { - var i = 0; - while (i < this.selection.length) { - var id = this.selection[i]; - if (!this.nodes[id]) { - this.selection.splice(i, 1); - } - else { - i++; - } + var i = 0; + while (i < this.selection.length) { + var id = this.selection[i]; + if (!this.nodes[id]) { + this.selection.splice(i, 1); + } + else { + i++; } + } }; /** @@ -14827,74 +14306,74 @@ Graph.prototype._updateSelection = function () { * @private */ Graph.prototype._getConnectionCount = function(level) { - if (level == undefined) { - level = 1; - } - - // get the nodes connected to given nodes - function getConnectedNodes(nodes) { - var connectedNodes = []; - - for (var j = 0, jMax = nodes.length; j < jMax; j++) { - var node = nodes[j]; - - // find all nodes connected to this node - var edges = node.edges; - for (var i = 0, iMax = edges.length; i < iMax; i++) { - var edge = edges[i]; - var other = null; - - // check if connected - if (edge.from == node) - other = edge.to; - else if (edge.to == node) - other = edge.from; - - // check if the other node is not already in the list with nodes - var k, kMax; - if (other) { - for (k = 0, kMax = nodes.length; k < kMax; k++) { - if (nodes[k] == other) { - other = null; - break; - } - } - } - if (other) { - for (k = 0, kMax = connectedNodes.length; k < kMax; k++) { - if (connectedNodes[k] == other) { - other = null; - break; - } - } - } + if (level == undefined) { + level = 1; + } - if (other) - connectedNodes.push(other); - } - } + // get the nodes connected to given nodes + function getConnectedNodes(nodes) { + var connectedNodes = []; - return connectedNodes; - } + for (var j = 0, jMax = nodes.length; j < jMax; j++) { + var node = nodes[j]; - var connections = []; - var nodes = this.nodes; - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - var c = [nodes[id]]; - for (var l = 0; l < level; l++) { - c = c.concat(getConnectedNodes(c)); + // find all nodes connected to this node + var edges = node.edges; + for (var i = 0, iMax = edges.length; i < iMax; i++) { + var edge = edges[i]; + var other = null; + + // check if connected + if (edge.from == node) + other = edge.to; + else if (edge.to == node) + other = edge.from; + + // check if the other node is not already in the list with nodes + var k, kMax; + if (other) { + for (k = 0, kMax = nodes.length; k < kMax; k++) { + if (nodes[k] == other) { + other = null; + break; + } + } + } + if (other) { + for (k = 0, kMax = connectedNodes.length; k < kMax; k++) { + if (connectedNodes[k] == other) { + other = null; + break; } - connections.push(c); + } } + + if (other) + connectedNodes.push(other); + } } - var hubs = []; - for (var i = 0, len = connections.length; i < len; i++) { - hubs.push(connections[i].length); + return connectedNodes; + } + + var connections = []; + var nodes = this.nodes; + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + var c = [nodes[id]]; + for (var l = 0; l < level; l++) { + c = c.concat(getConnectedNodes(c)); + } + connections.push(c); } + } + + var hubs = []; + for (var i = 0, len = connections.length; i < len; i++) { + hubs.push(connections[i].length); + } - return hubs; + return hubs; }; @@ -14906,14 +14385,14 @@ Graph.prototype._getConnectionCount = function(level) { * or '30%') */ Graph.prototype.setSize = function(width, height) { - this.frame.style.width = width; - this.frame.style.height = height; + this.frame.style.width = width; + this.frame.style.height = height; - this.frame.canvas.style.width = '100%'; - this.frame.canvas.style.height = '100%'; + this.frame.canvas.style.width = '100%'; + this.frame.canvas.style.height = '100%'; - this.frame.canvas.width = this.frame.canvas.clientWidth; - this.frame.canvas.height = this.frame.canvas.clientHeight; + this.frame.canvas.width = this.frame.canvas.clientWidth; + this.frame.canvas.height = this.frame.canvas.clientHeight; }; /** @@ -14922,45 +14401,45 @@ Graph.prototype.setSize = function(width, height) { * @private */ Graph.prototype._setNodes = function(nodes) { - var oldNodesData = this.nodesData; + var oldNodesData = this.nodesData; - if (nodes instanceof DataSet || nodes instanceof DataView) { - this.nodesData = nodes; - } - else if (nodes instanceof Array) { - this.nodesData = new DataSet(); - this.nodesData.add(nodes); - } - else if (!nodes) { - this.nodesData = new DataSet(); - } - else { - throw new TypeError('Array or DataSet expected'); - } + if (nodes instanceof DataSet || nodes instanceof DataView) { + this.nodesData = nodes; + } + else if (nodes instanceof Array) { + this.nodesData = new DataSet(); + this.nodesData.add(nodes); + } + else if (!nodes) { + this.nodesData = new DataSet(); + } + else { + throw new TypeError('Array or DataSet expected'); + } - if (oldNodesData) { - // unsubscribe from old dataset - util.forEach(this.nodesListeners, function (callback, event) { - oldNodesData.unsubscribe(event, callback); - }); - } + if (oldNodesData) { + // unsubscribe from old dataset + util.forEach(this.nodesListeners, function (callback, event) { + oldNodesData.unsubscribe(event, callback); + }); + } - // remove drawn nodes - this.nodes = {}; + // remove drawn nodes + this.nodes = {}; - if (this.nodesData) { - // subscribe to new dataset - var me = this; - util.forEach(this.nodesListeners, function (callback, event) { - me.nodesData.subscribe(event, callback); - }); + if (this.nodesData) { + // subscribe to new dataset + var me = this; + util.forEach(this.nodesListeners, function (callback, event) { + me.nodesData.subscribe(event, callback); + }); - // draw all new nodes - var ids = this.nodesData.getIds(); - this._addNodes(ids); - } + // draw all new nodes + var ids = this.nodesData.getIds(); + this._addNodes(ids); + } - this._updateSelection(); + this._updateSelection(); }; /** @@ -14969,29 +14448,29 @@ Graph.prototype._setNodes = function(nodes) { * @private */ Graph.prototype._addNodes = function(ids) { - var id; - for (var i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - var data = this.nodesData.get(id); - var node = new Node(data, this.images, this.groups, this.constants); - this.nodes[id] = node; // note: this may replace an existing node - - if (!node.isFixed()) { - // TODO: position new nodes in a smarter way! - var radius = this.constants.edges.length * 2; - var count = ids.length; - var angle = 2 * Math.PI * (i / count); - node.x = radius * Math.cos(angle); - node.y = radius * Math.sin(angle); - - // note: no not use node.isMoving() here, as that gives the current - // velocity of the node, which is zero after creation of the node. - this.moving = true; - } + var id; + for (var i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + var data = this.nodesData.get(id); + var node = new Node(data, this.images, this.groups, this.constants); + this.nodes[id] = node; // note: this may replace an existing node + + if (!node.isFixed()) { + // TODO: position new nodes in a smarter way! + var radius = this.constants.edges.length * 2; + var count = ids.length; + var angle = 2 * Math.PI * (i / count); + node.x = radius * Math.cos(angle); + node.y = radius * Math.sin(angle); + + // note: no not use node.isMoving() here, as that gives the current + // velocity of the node, which is zero after creation of the node. + this.moving = true; } + } - this._reconnectEdges(); - this._updateValueRange(this.nodes); + this._reconnectEdges(); + this._updateValueRange(this.nodes); }; /** @@ -15000,29 +14479,29 @@ Graph.prototype._addNodes = function(ids) { * @private */ Graph.prototype._updateNodes = function(ids) { - var nodes = this.nodes, - nodesData = this.nodesData; - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; - var node = nodes[id]; - var data = nodesData.get(id); - if (node) { - // update node - node.setProperties(data, this.constants); - } - else { - // create node - node = new Node(properties, this.images, this.groups, this.constants); - nodes[id] = node; + var nodes = this.nodes, + nodesData = this.nodesData; + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; + var node = nodes[id]; + var data = nodesData.get(id); + if (node) { + // update node + node.setProperties(data, this.constants); + } + else { + // create node + node = new Node(properties, this.images, this.groups, this.constants); + nodes[id] = node; - if (!node.isFixed()) { - this.moving = true; - } - } + if (!node.isFixed()) { + this.moving = true; + } } + } - this._reconnectEdges(); - this._updateValueRange(nodes); + this._reconnectEdges(); + this._updateValueRange(nodes); }; /** @@ -15031,15 +14510,15 @@ Graph.prototype._updateNodes = function(ids) { * @private */ Graph.prototype._removeNodes = function(ids) { - var nodes = this.nodes; - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; - delete nodes[id]; - } + var nodes = this.nodes; + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; + delete nodes[id]; + } - this._reconnectEdges(); - this._updateSelection(); - this._updateValueRange(nodes); + this._reconnectEdges(); + this._updateSelection(); + this._updateValueRange(nodes); }; /** @@ -15049,45 +14528,45 @@ Graph.prototype._removeNodes = function(ids) { * @private */ Graph.prototype._setEdges = function(edges) { - var oldEdgesData = this.edgesData; + var oldEdgesData = this.edgesData; - if (edges instanceof DataSet || edges instanceof DataView) { - this.edgesData = edges; - } - else if (edges instanceof Array) { - this.edgesData = new DataSet(); - this.edgesData.add(edges); - } - else if (!edges) { - this.edgesData = new DataSet(); - } - else { - throw new TypeError('Array or DataSet expected'); - } + if (edges instanceof DataSet || edges instanceof DataView) { + this.edgesData = edges; + } + else if (edges instanceof Array) { + this.edgesData = new DataSet(); + this.edgesData.add(edges); + } + else if (!edges) { + this.edgesData = new DataSet(); + } + else { + throw new TypeError('Array or DataSet expected'); + } - if (oldEdgesData) { - // unsubscribe from old dataset - util.forEach(this.edgesListeners, function (callback, event) { - oldEdgesData.unsubscribe(event, callback); - }); - } + if (oldEdgesData) { + // unsubscribe from old dataset + util.forEach(this.edgesListeners, function (callback, event) { + oldEdgesData.unsubscribe(event, callback); + }); + } - // remove drawn edges - this.edges = {}; + // remove drawn edges + this.edges = {}; - if (this.edgesData) { - // subscribe to new dataset - var me = this; - util.forEach(this.edgesListeners, function (callback, event) { - me.edgesData.subscribe(event, callback); - }); + if (this.edgesData) { + // subscribe to new dataset + var me = this; + util.forEach(this.edgesListeners, function (callback, event) { + me.edgesData.subscribe(event, callback); + }); - // draw all new nodes - var ids = this.edgesData.getIds(); - this._addEdges(ids); - } + // draw all new nodes + var ids = this.edgesData.getIds(); + this._addEdges(ids); + } - this._reconnectEdges(); + this._reconnectEdges(); }; /** @@ -15096,22 +14575,22 @@ Graph.prototype._setEdges = function(edges) { * @private */ Graph.prototype._addEdges = function (ids) { - var edges = this.edges, - edgesData = this.edgesData; - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; + var edges = this.edges, + edgesData = this.edgesData; + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; - var oldEdge = edges[id]; - if (oldEdge) { - oldEdge.disconnect(); - } - - var data = edgesData.get(id); - edges[id] = new Edge(data, this, this.constants); + var oldEdge = edges[id]; + if (oldEdge) { + oldEdge.disconnect(); } - this.moving = true; - this._updateValueRange(edges); + var data = edgesData.get(id); + edges[id] = new Edge(data, this, this.constants); + } + + this.moving = true; + this._updateValueRange(edges); }; /** @@ -15120,28 +14599,28 @@ Graph.prototype._addEdges = function (ids) { * @private */ Graph.prototype._updateEdges = function (ids) { - var edges = this.edges, - edgesData = this.edgesData; - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; - - var data = edgesData.get(id); - var edge = edges[id]; - if (edge) { - // update edge - edge.disconnect(); - edge.setProperties(data, this.constants); - edge.connect(); - } - else { - // create edge - edge = new Edge(data, this, this.constants); - this.edges[id] = edge; - } + var edges = this.edges, + edgesData = this.edgesData; + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; + + var data = edgesData.get(id); + var edge = edges[id]; + if (edge) { + // update edge + edge.disconnect(); + edge.setProperties(data, this.constants); + edge.connect(); + } + else { + // create edge + edge = new Edge(data, this, this.constants); + this.edges[id] = edge; } + } - this.moving = true; - this._updateValueRange(edges); + this.moving = true; + this._updateValueRange(edges); }; /** @@ -15150,18 +14629,18 @@ Graph.prototype._updateEdges = function (ids) { * @private */ Graph.prototype._removeEdges = function (ids) { - var edges = this.edges; - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; - var edge = edges[id]; - if (edge) { - edge.disconnect(); - delete edges[id]; - } + var edges = this.edges; + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; + var edge = edges[id]; + if (edge) { + edge.disconnect(); + delete edges[id]; } + } - this.moving = true; - this._updateValueRange(edges); + this.moving = true; + this._updateValueRange(edges); }; /** @@ -15169,23 +14648,23 @@ Graph.prototype._removeEdges = function (ids) { * @private */ Graph.prototype._reconnectEdges = function() { - var id, - nodes = this.nodes, - edges = this.edges; - for (id in nodes) { - if (nodes.hasOwnProperty(id)) { - nodes[id].edges = []; - } + var id, + nodes = this.nodes, + edges = this.edges; + for (id in nodes) { + if (nodes.hasOwnProperty(id)) { + nodes[id].edges = []; } + } - for (id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; - edge.from = null; - edge.to = null; - edge.connect(); - } + for (id in edges) { + if (edges.hasOwnProperty(id)) { + var edge = edges[id]; + edge.from = null; + edge.to = null; + edge.connect(); } + } }; /** @@ -15197,29 +14676,29 @@ Graph.prototype._reconnectEdges = function() { * @private */ Graph.prototype._updateValueRange = function(obj) { - var id; - - // determine the range of the objects - var valueMin = undefined; - var valueMax = undefined; - for (id in obj) { - if (obj.hasOwnProperty(id)) { - var value = obj[id].getValue(); - if (value !== undefined) { - valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin); - valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax); - } - } + var id; + + // determine the range of the objects + var valueMin = undefined; + var valueMax = undefined; + for (id in obj) { + if (obj.hasOwnProperty(id)) { + var value = obj[id].getValue(); + if (value !== undefined) { + valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin); + valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax); + } } + } - // adjust the range of all objects - if (valueMin !== undefined && valueMax !== undefined) { - for (id in obj) { - if (obj.hasOwnProperty(id)) { - obj[id].setValueRange(valueMin, valueMax); - } - } + // adjust the range of all objects + if (valueMin !== undefined && valueMax !== undefined) { + for (id in obj) { + if (obj.hasOwnProperty(id)) { + obj[id].setValueRange(valueMin, valueMax); + } } + } }; /** @@ -15227,9 +14706,9 @@ Graph.prototype._updateValueRange = function(obj) { * chart will be resized too. */ Graph.prototype.redraw = function() { - this.setSize(this.width, this.height); + this.setSize(this.width, this.height); - this._redraw(); + this._redraw(); }; /** @@ -15237,23 +14716,23 @@ Graph.prototype.redraw = function() { * @private */ Graph.prototype._redraw = function() { - var ctx = this.frame.canvas.getContext('2d'); + var ctx = this.frame.canvas.getContext('2d'); - // clear the canvas - var w = this.frame.canvas.width; - var h = this.frame.canvas.height; - ctx.clearRect(0, 0, w, h); + // clear the canvas + var w = this.frame.canvas.width; + var h = this.frame.canvas.height; + ctx.clearRect(0, 0, w, h); - // set scaling and translation - ctx.save(); - ctx.translate(this.translation.x, this.translation.y); - ctx.scale(this.scale, this.scale); + // set scaling and translation + ctx.save(); + ctx.translate(this.translation.x, this.translation.y); + ctx.scale(this.scale, this.scale); - this._drawEdges(ctx); - this._drawNodes(ctx); + this._drawEdges(ctx); + this._drawNodes(ctx); - // restore original scaling and translation - ctx.restore(); + // restore original scaling and translation + ctx.restore(); }; /** @@ -15263,19 +14742,19 @@ Graph.prototype._redraw = function() { * @private */ Graph.prototype._setTranslation = function(offsetX, offsetY) { - if (this.translation === undefined) { - this.translation = { - x: 0, - y: 0 - }; - } + if (this.translation === undefined) { + this.translation = { + x: 0, + y: 0 + }; + } - if (offsetX !== undefined) { - this.translation.x = offsetX; - } - if (offsetY !== undefined) { - this.translation.y = offsetY; - } + if (offsetX !== undefined) { + this.translation.x = offsetX; + } + if (offsetY !== undefined) { + this.translation.y = offsetY; + } }; /** @@ -15284,10 +14763,10 @@ Graph.prototype._setTranslation = function(offsetX, offsetY) { * @private */ Graph.prototype._getTranslation = function() { - return { - x: this.translation.x, - y: this.translation.y - }; + return { + x: this.translation.x, + y: this.translation.y + }; }; /** @@ -15296,7 +14775,7 @@ Graph.prototype._getTranslation = function() { * @private */ Graph.prototype._setScale = function(scale) { - this.scale = scale; + this.scale = scale; }; /** * Get the current scale of the graph @@ -15304,7 +14783,7 @@ Graph.prototype._setScale = function(scale) { * @private */ Graph.prototype._getScale = function() { - return this.scale; + return this.scale; }; /** @@ -15314,7 +14793,7 @@ Graph.prototype._getScale = function() { * @private */ Graph.prototype._canvasToX = function(x) { - return (x - this.translation.x) / this.scale; + return (x - this.translation.x) / this.scale; }; /** @@ -15324,7 +14803,7 @@ Graph.prototype._canvasToX = function(x) { * @private */ Graph.prototype._xToCanvas = function(x) { - return x * this.scale + this.translation.x; + return x * this.scale + this.translation.x; }; /** @@ -15334,7 +14813,7 @@ Graph.prototype._xToCanvas = function(x) { * @private */ Graph.prototype._canvasToY = function(y) { - return (y - this.translation.y) / this.scale; + return (y - this.translation.y) / this.scale; }; /** @@ -15344,7 +14823,7 @@ Graph.prototype._canvasToY = function(y) { * @private */ Graph.prototype._yToCanvas = function(y) { - return y * this.scale + this.translation.y ; + return y * this.scale + this.translation.y ; }; /** @@ -15354,24 +14833,24 @@ Graph.prototype._yToCanvas = function(y) { * @private */ Graph.prototype._drawNodes = function(ctx) { - // first draw the unselected nodes - var nodes = this.nodes; - var selected = []; - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - if (nodes[id].isSelected()) { - selected.push(id); - } - else { - nodes[id].draw(ctx); - } - } + // first draw the unselected nodes + var nodes = this.nodes; + var selected = []; + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + if (nodes[id].isSelected()) { + selected.push(id); + } + else { + nodes[id].draw(ctx); + } } + } - // draw the selected nodes on top - for (var s = 0, sMax = selected.length; s < sMax; s++) { - nodes[selected[s]].draw(ctx); - } + // draw the selected nodes on top + for (var s = 0, sMax = selected.length; s < sMax; s++) { + nodes[selected[s]].draw(ctx); + } }; /** @@ -15381,15 +14860,15 @@ Graph.prototype._drawNodes = function(ctx) { * @private */ Graph.prototype._drawEdges = function(ctx) { - var edges = this.edges; - for (var id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; - if (edge.connected) { - edges[id].draw(ctx); - } - } + var edges = this.edges; + for (var id in edges) { + if (edges.hasOwnProperty(id)) { + var edge = edges[id]; + if (edge.connected) { + edges[id].draw(ctx); + } } + } }; /** @@ -15397,22 +14876,22 @@ Graph.prototype._drawEdges = function(ctx) { * @private */ Graph.prototype._doStabilize = function() { - var start = new Date(); - - // find stable position - var count = 0; - var vmin = this.constants.minVelocity; - var stable = false; - while (!stable && count < this.constants.maxIterations) { - this._calculateForces(); - this._discreteStepNodes(); - stable = !this._isMoving(vmin); - count++; - } + var start = new Date(); + + // find stable position + var count = 0; + var vmin = this.constants.minVelocity; + var stable = false; + while (!stable && count < this.constants.maxIterations) { + this._calculateForces(); + this._discreteStepNodes(); + stable = !this._isMoving(vmin); + count++; + } - var end = new Date(); + var end = new Date(); - // console.log('Stabilized in ' + (end-start) + ' ms, ' + count + ' iterations' ); // TODO: cleanup + // console.log('Stabilized in ' + (end-start) + ' ms, ' + count + ' iterations' ); // TODO: cleanup }; /** @@ -15421,149 +14900,149 @@ Graph.prototype._doStabilize = function() { * @private */ Graph.prototype._calculateForces = function() { - // create a local edge to the nodes and edges, that is faster - var id, dx, dy, angle, distance, fx, fy, - repulsingForce, springForce, length, edgeLength, - nodes = this.nodes, - edges = this.edges; - - // gravity, add a small constant force to pull the nodes towards the center of - // the graph - // Also, the forces are reset to zero in this loop by using _setForce instead - // of _addForce - var gravity = 0.01, - gx = this.frame.canvas.clientWidth / 2, - gy = this.frame.canvas.clientHeight / 2; - for (id in nodes) { - if (nodes.hasOwnProperty(id)) { - var node = nodes[id]; - dx = gx - node.x; - dy = gy - node.y; - angle = Math.atan2(dy, dx); - fx = Math.cos(angle) * gravity; - fy = Math.sin(angle) * gravity; - - node._setForce(fx, fy); - } - } - - // repulsing forces between nodes - var minimumDistance = this.constants.nodes.distance, - steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance - - for (var id1 in nodes) { - if (nodes.hasOwnProperty(id1)) { - var node1 = nodes[id1]; - for (var id2 in nodes) { - if (nodes.hasOwnProperty(id2)) { - var node2 = nodes[id2]; - // calculate normally distributed force - dx = node2.x - node1.x; - dy = node2.y - node1.y; - distance = Math.sqrt(dx * dx + dy * dy); - angle = Math.atan2(dy, dx); - - // TODO: correct factor for repulsing force - //repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force - //repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force - repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force - fx = Math.cos(angle) * repulsingForce; - fy = Math.sin(angle) * repulsingForce; - - node1._addForce(-fx, -fy); - node2._addForce(fx, fy); - } - } - } + // create a local edge to the nodes and edges, that is faster + var id, dx, dy, angle, distance, fx, fy, + repulsingForce, springForce, length, edgeLength, + nodes = this.nodes, + edges = this.edges; + + // gravity, add a small constant force to pull the nodes towards the center of + // the graph + // Also, the forces are reset to zero in this loop by using _setForce instead + // of _addForce + var gravity = 0.01, + gx = this.frame.canvas.clientWidth / 2, + gy = this.frame.canvas.clientHeight / 2; + for (id in nodes) { + if (nodes.hasOwnProperty(id)) { + var node = nodes[id]; + dx = gx - node.x; + dy = gy - node.y; + angle = Math.atan2(dy, dx); + fx = Math.cos(angle) * gravity; + fy = Math.sin(angle) * gravity; + + node._setForce(fx, fy); } + } - /* TODO: re-implement repulsion of edges - for (var n = 0; n < nodes.length; n++) { - for (var l = 0; l < edges.length; l++) { - var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2, - ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2, - - // calculate normally distributed force - dx = nodes[n].x - lx, - dy = nodes[n].y - ly, - distance = Math.sqrt(dx * dx + dy * dy), - angle = Math.atan2(dy, dx), - - - // TODO: correct factor for repulsing force - //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force - //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force - repulsingforce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)), // TODO: customize the repulsing force - fx = Math.cos(angle) * repulsingforce, - fy = Math.sin(angle) * repulsingforce; - nodes[n]._addForce(fx, fy); - edges[l].from._addForce(-fx/2,-fy/2); - edges[l].to._addForce(-fx/2,-fy/2); - } + // repulsing forces between nodes + var minimumDistance = this.constants.nodes.distance, + steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance + + for (var id1 in nodes) { + if (nodes.hasOwnProperty(id1)) { + var node1 = nodes[id1]; + for (var id2 in nodes) { + if (nodes.hasOwnProperty(id2)) { + var node2 = nodes[id2]; + // calculate normally distributed force + dx = node2.x - node1.x; + dy = node2.y - node1.y; + distance = Math.sqrt(dx * dx + dy * dy); + angle = Math.atan2(dy, dx); + + // TODO: correct factor for repulsing force + //repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + //repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force + fx = Math.cos(angle) * repulsingForce; + fy = Math.sin(angle) * repulsingForce; + + node1._addForce(-fx, -fy); + node2._addForce(fx, fy); + } + } } - */ + } - // forces caused by the edges, modelled as springs - for (id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; - if (edge.connected) { - dx = (edge.to.x - edge.from.x); - dy = (edge.to.y - edge.from.y); - //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin - //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin - //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2; - edgeLength = edge.length; - length = Math.sqrt(dx * dx + dy * dy); - angle = Math.atan2(dy, dx); - - springForce = edge.stiffness * (edgeLength - length); - - fx = Math.cos(angle) * springForce; - fy = Math.sin(angle) * springForce; - - edge.from._addForce(-fx, -fy); - edge.to._addForce(fx, fy); - } - } + /* TODO: re-implement repulsion of edges + for (var n = 0; n < nodes.length; n++) { + for (var l = 0; l < edges.length; l++) { + var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2, + ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2, + + // calculate normally distributed force + dx = nodes[n].x - lx, + dy = nodes[n].y - ly, + distance = Math.sqrt(dx * dx + dy * dy), + angle = Math.atan2(dy, dx), + + + // TODO: correct factor for repulsing force + //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force + repulsingforce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)), // TODO: customize the repulsing force + fx = Math.cos(angle) * repulsingforce, + fy = Math.sin(angle) * repulsingforce; + nodes[n]._addForce(fx, fy); + edges[l].from._addForce(-fx/2,-fy/2); + edges[l].to._addForce(-fx/2,-fy/2); + } + } + */ + + // forces caused by the edges, modelled as springs + for (id in edges) { + if (edges.hasOwnProperty(id)) { + var edge = edges[id]; + if (edge.connected) { + dx = (edge.to.x - edge.from.x); + dy = (edge.to.y - edge.from.y); + //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin + //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin + //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2; + edgeLength = edge.length; + length = Math.sqrt(dx * dx + dy * dy); + angle = Math.atan2(dy, dx); + + springForce = edge.stiffness * (edgeLength - length); + + fx = Math.cos(angle) * springForce; + fy = Math.sin(angle) * springForce; + + edge.from._addForce(-fx, -fy); + edge.to._addForce(fx, fy); + } } + } - /* TODO: re-implement repulsion of edges - // repulsing forces between edges - var minimumDistance = this.constants.edges.distance, - steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance - for (var l = 0; l < edges.length; l++) { - //Keep distance from other edge centers - for (var l2 = l + 1; l2 < this.edges.length; l2++) { - //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin - //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin - //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0), - var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2, - ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2, - l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2, - l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2, - - // calculate normally distributed force - dx = l2x - lx, - dy = l2y - ly, - distance = Math.sqrt(dx * dx + dy * dy), - angle = Math.atan2(dy, dx), - - - // TODO: correct factor for repulsing force - //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force - //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force - repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force - fx = Math.cos(angle) * repulsingforce, - fy = Math.sin(angle) * repulsingforce; - - edges[l].from._addForce(-fx, -fy); - edges[l].to._addForce(-fx, -fy); - edges[l2].from._addForce(fx, fy); - edges[l2].to._addForce(fx, fy); - } - } - */ + /* TODO: re-implement repulsion of edges + // repulsing forces between edges + var minimumDistance = this.constants.edges.distance, + steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance + for (var l = 0; l < edges.length; l++) { + //Keep distance from other edge centers + for (var l2 = l + 1; l2 < this.edges.length; l2++) { + //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin + //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin + //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0), + var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2, + ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2, + l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2, + l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2, + + // calculate normally distributed force + dx = l2x - lx, + dy = l2y - ly, + distance = Math.sqrt(dx * dx + dy * dy), + angle = Math.atan2(dy, dx), + + + // TODO: correct factor for repulsing force + //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force + repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force + fx = Math.cos(angle) * repulsingforce, + fy = Math.sin(angle) * repulsingforce; + + edges[l].from._addForce(-fx, -fy); + edges[l].to._addForce(-fx, -fy); + edges[l2].from._addForce(fx, fy); + edges[l2].to._addForce(fx, fy); + } + } + */ }; @@ -15574,14 +15053,14 @@ Graph.prototype._calculateForces = function() { * @private */ Graph.prototype._isMoving = function(vmin) { - // TODO: ismoving does not work well: should check the kinetic energy, not its velocity - var nodes = this.nodes; - for (var id in nodes) { - if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) { - return true; - } + // TODO: ismoving does not work well: should check the kinetic energy, not its velocity + var nodes = this.nodes; + for (var id in nodes) { + if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) { + return true; } - return false; + } + return false; }; @@ -15590,124 +15069,124 @@ Graph.prototype._isMoving = function(vmin) { * @private */ Graph.prototype._discreteStepNodes = function() { - var interval = this.refreshRate / 1000.0; // in seconds - var nodes = this.nodes; - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - nodes[id].discreteStep(interval); - } + var interval = this.refreshRate / 1000.0; // in seconds + var nodes = this.nodes; + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + nodes[id].discreteStep(interval); } + } }; /** * Start animating nodes and edges */ Graph.prototype.start = function() { - if (this.moving) { - this._calculateForces(); - this._discreteStepNodes(); + if (this.moving) { + this._calculateForces(); + this._discreteStepNodes(); - var vmin = this.constants.minVelocity; - this.moving = this._isMoving(vmin); - } + var vmin = this.constants.minVelocity; + this.moving = this._isMoving(vmin); + } - if (this.moving) { - // start animation. only start timer if it is not already running - if (!this.timer) { - var graph = this; - this.timer = window.setTimeout(function () { - graph.timer = undefined; - graph.start(); - graph._redraw(); - }, this.refreshRate); - } - } - else { - this._redraw(); + if (this.moving) { + // start animation. only start timer if it is not already running + if (!this.timer) { + var graph = this; + this.timer = window.setTimeout(function () { + graph.timer = undefined; + graph.start(); + graph._redraw(); + }, this.refreshRate); } + } + else { + this._redraw(); + } }; /** * Stop animating nodes and edges. */ Graph.prototype.stop = function () { - if (this.timer) { - window.clearInterval(this.timer); - this.timer = undefined; - } + if (this.timer) { + window.clearInterval(this.timer); + this.timer = undefined; + } }; /** * vis.js module exports */ var vis = { - util: util, - events: events, - - Controller: Controller, - DataSet: DataSet, - DataView: DataView, - Range: Range, - Stack: Stack, - TimeStep: TimeStep, - EventBus: EventBus, - - components: { - items: { - Item: Item, - ItemBox: ItemBox, - ItemPoint: ItemPoint, - ItemRange: ItemRange - }, - - Component: Component, - Panel: Panel, - RootPanel: RootPanel, - ItemSet: ItemSet, - TimeAxis: TimeAxis + util: util, + events: events, + + Controller: Controller, + DataSet: DataSet, + DataView: DataView, + Range: Range, + Stack: Stack, + TimeStep: TimeStep, + EventBus: EventBus, + + components: { + items: { + Item: Item, + ItemBox: ItemBox, + ItemPoint: ItemPoint, + ItemRange: ItemRange }, - graph: { - Node: Node, - Edge: Edge, - Popup: Popup, - Groups: Groups, - Images: Images - }, + Component: Component, + Panel: Panel, + RootPanel: RootPanel, + ItemSet: ItemSet, + TimeAxis: TimeAxis + }, - Timeline: Timeline, - Graph: Graph + graph: { + Node: Node, + Edge: Edge, + Popup: Popup, + Groups: Groups, + Images: Images + }, + + Timeline: Timeline, + Graph: Graph }; /** * CommonJS module exports */ if (typeof exports !== 'undefined') { - exports = vis; + exports = vis; } if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { - module.exports = vis; + module.exports = vis; } /** * AMD module exports */ if (typeof(define) === 'function') { - define(function () { - return vis; - }); + define(function () { + return vis; + }); } /** * Window exports */ if (typeof window !== 'undefined') { - // attach the module to the window, load as a regular javascript file - window['vis'] = vis; + // attach the module to the window, load as a regular javascript file + window['vis'] = vis; } // inject css -util.loadCss("/* vis.js stylesheet */\n.vis.timeline {\n}\n\n\n.vis.timeline.rootpanel {\n position: relative;\n overflow: hidden;\n\n border: 1px solid #bfbfbf;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n\n.vis.timeline .panel {\n position: absolute;\n overflow: hidden;\n}\n\n\n.vis.timeline .groupset {\n position: absolute;\n padding: 0;\n margin: 0;\n}\n\n.vis.timeline .labels {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n\n padding: 0;\n margin: 0;\n\n border-right: 1px solid #bfbfbf;\n box-sizing: border-box;\n -moz-box-sizing: border-box;\n}\n\n.vis.timeline .labels .label {\n position: absolute;\n left: 0;\n top: 0;\n width: 100%;\n border-bottom: 1px solid #bfbfbf;\n color: #4d4d4d;\n}\n\n.vis.timeline .labels .label .inner {\n display: inline-block;\n padding: 5px;\n}\n\n\n.vis.timeline .itemset {\n position: absolute;\n padding: 0;\n margin: 0;\n overflow: hidden;\n}\n\n.vis.timeline .background {\n}\n\n.vis.timeline .foreground {\n}\n\n.vis.timeline .itemset-axis {\n position: absolute;\n}\n\n.vis.timeline .groupset .itemset-axis {\n border-top: 1px solid #bfbfbf;\n}\n\n/* TODO: with orientation=='bottom', this will more or less overlap with timeline axis\n.vis.timeline .groupset .itemset-axis:last-child {\n border-top: none;\n}\n*/\n\n\n.vis.timeline .item {\n position: absolute;\n color: #1A1A1A;\n border-color: #97B0F8;\n background-color: #D5DDF6;\n display: inline-block;\n}\n\n.vis.timeline .item.selected {\n border-color: #FFC200;\n background-color: #FFF785;\n z-index: 999;\n}\n\n.vis.timeline .item.cluster {\n /* TODO: use another color or pattern? */\n background: #97B0F8 url('img/cluster_bg.png');\n color: white;\n}\n.vis.timeline .item.cluster.point {\n border-color: #D5DDF6;\n}\n\n.vis.timeline .item.box {\n text-align: center;\n border-style: solid;\n border-width: 1px;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.point {\n background: none;\n}\n\n.vis.timeline .dot {\n border: 5px solid #97B0F8;\n position: absolute;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.range {\n overflow: hidden;\n border-style: solid;\n border-width: 1px;\n border-radius: 2px;\n -moz-border-radius: 2px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.rangeoverflow {\n border-style: solid;\n border-width: 1px;\n border-radius: 2px;\n -moz-border-radius: 2px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.range .drag-left, .vis.timeline .item.rangeoverflow .drag-left {\n cursor: w-resize;\n z-index: 1000;\n}\n\n.vis.timeline .item.range .drag-right, .vis.timeline .item.rangeoverflow .drag-right {\n cursor: e-resize;\n z-index: 1000;\n}\n\n.vis.timeline .item.range .content, .vis.timeline .item.rangeoverflow .content {\n position: relative;\n display: inline-block;\n}\n\n.vis.timeline .item.line {\n position: absolute;\n width: 0;\n border-left-width: 1px;\n border-left-style: solid;\n}\n\n.vis.timeline .item .content {\n margin: 5px;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.vis.timeline .axis {\n position: relative;\n}\n\n.vis.timeline .axis .text {\n position: absolute;\n color: #4d4d4d;\n padding: 3px;\n white-space: nowrap;\n}\n\n.vis.timeline .axis .text.measure {\n position: absolute;\n padding-left: 0;\n padding-right: 0;\n margin-left: 0;\n margin-right: 0;\n visibility: hidden;\n}\n\n.vis.timeline .axis .grid.vertical {\n position: absolute;\n width: 0;\n border-right: 1px solid;\n}\n\n.vis.timeline .axis .grid.horizontal {\n position: absolute;\n left: 0;\n width: 100%;\n height: 0;\n border-bottom: 1px solid;\n}\n\n.vis.timeline .axis .grid.minor {\n border-color: #e5e5e5;\n}\n\n.vis.timeline .axis .grid.major {\n border-color: #bfbfbf;\n}\n\n.vis.timeline .currenttime {\n background-color: #FF7F6E;\n width: 2px;\n z-index: 9;\n}\n.vis.timeline .customtime {\n background-color: #6E94FF;\n width: 2px;\n cursor: move;\n z-index: 9; \n}\n"); +util.loadCss("/* vis.js stylesheet */\n.vis.timeline {\n}\n\n\n.vis.timeline.rootpanel {\n position: relative;\n overflow: hidden;\n\n border: 1px solid #bfbfbf;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n\n.vis.timeline .panel {\n position: absolute;\n overflow: hidden;\n}\n\n\n.vis.timeline .groupset {\n position: absolute;\n padding: 0;\n margin: 0;\n}\n\n.vis.timeline .labels {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n\n padding: 0;\n margin: 0;\n\n border-right: 1px solid #bfbfbf;\n box-sizing: border-box;\n -moz-box-sizing: border-box;\n}\n\n.vis.timeline .labels .label-set {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n\n overflow: hidden;\n\n border-top: none;\n border-bottom: 1px solid #bfbfbf;\n}\n\n.vis.timeline .labels .label-set .label {\n position: absolute;\n left: 0;\n top: 0;\n width: 100%;\n color: #4d4d4d;\n}\n\n.vis.timeline.top .labels .label-set .label,\n.vis.timeline.top .groupset .itemset-axis {\n border-top: 1px solid #bfbfbf;\n border-bottom: none;\n}\n\n.vis.timeline.bottom .labels .label-set .label,\n.vis.timeline.bottom .groupset .itemset-axis {\n border-top: none;\n border-bottom: 1px solid #bfbfbf;\n}\n\n.vis.timeline .labels .label-set .label .inner {\n display: inline-block;\n padding: 5px;\n}\n\n\n.vis.timeline .itemset {\n position: absolute;\n padding: 0;\n margin: 0;\n overflow: hidden;\n}\n\n.vis.timeline .background {\n}\n\n.vis.timeline .foreground {\n}\n\n.vis.timeline .itemset-axis {\n position: absolute;\n}\n\n\n.vis.timeline .item {\n position: absolute;\n color: #1A1A1A;\n border-color: #97B0F8;\n background-color: #D5DDF6;\n display: inline-block;\n}\n\n.vis.timeline .item.selected {\n border-color: #FFC200;\n background-color: #FFF785;\n z-index: 999;\n}\n\n.vis.timeline .item.cluster {\n /* TODO: use another color or pattern? */\n background: #97B0F8 url('img/cluster_bg.png');\n color: white;\n}\n.vis.timeline .item.cluster.point {\n border-color: #D5DDF6;\n}\n\n.vis.timeline .item.box {\n text-align: center;\n border-style: solid;\n border-width: 1px;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.point {\n background: none;\n}\n\n.vis.timeline .dot {\n border: 5px solid #97B0F8;\n position: absolute;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.range {\n overflow: hidden;\n border-style: solid;\n border-width: 1px;\n border-radius: 2px;\n -moz-border-radius: 2px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.rangeoverflow {\n border-style: solid;\n border-width: 1px;\n border-radius: 2px;\n -moz-border-radius: 2px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.range .drag-left, .vis.timeline .item.rangeoverflow .drag-left {\n cursor: w-resize;\n z-index: 1000;\n}\n\n.vis.timeline .item.range .drag-right, .vis.timeline .item.rangeoverflow .drag-right {\n cursor: e-resize;\n z-index: 1000;\n}\n\n.vis.timeline .item.range .content, .vis.timeline .item.rangeoverflow .content {\n position: relative;\n display: inline-block;\n}\n\n.vis.timeline .item.line {\n position: absolute;\n width: 0;\n border-left-width: 1px;\n border-left-style: solid;\n}\n\n.vis.timeline .item .content {\n margin: 5px;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.vis.timeline .axis {\n position: relative;\n}\n\n.vis.timeline .axis .text {\n position: absolute;\n color: #4d4d4d;\n padding: 3px;\n white-space: nowrap;\n}\n\n.vis.timeline .axis .text.measure {\n position: absolute;\n padding-left: 0;\n padding-right: 0;\n margin-left: 0;\n margin-right: 0;\n visibility: hidden;\n}\n\n.vis.timeline .axis .grid.vertical {\n position: absolute;\n width: 0;\n border-right: 1px solid;\n}\n\n.vis.timeline .axis .grid.horizontal {\n position: absolute;\n left: 0;\n width: 100%;\n height: 0;\n border-bottom: 1px solid;\n}\n\n.vis.timeline .axis .grid.minor {\n border-color: #e5e5e5;\n}\n\n.vis.timeline .axis .grid.major {\n border-color: #bfbfbf;\n}\n\n.vis.timeline .currenttime {\n background-color: #FF7F6E;\n width: 2px;\n z-index: 9;\n}\n.vis.timeline .customtime {\n background-color: #6E94FF;\n width: 2px;\n cursor: move;\n z-index: 9;\n}\n"); },{"hammerjs":1,"moment":2}]},{},[3]) (3) diff --git a/vis.min.js b/vis.min.js index f58a6d0e..843cd31c 100644 --- a/vis.min.js +++ b/vis.min.js @@ -5,7 +5,7 @@ * A dynamic, browser-based visualization library. * * @version 0.3.0-SNAPSHOT - * @date 2013-10-30 + * @date 2014-01-03 * * @license * Copyright (C) 2011-2013 Almende B.V, http://almende.com @@ -22,9 +22,8 @@ * License for the specific language governing permissions and limitations under * the License. */ -!function(t){"object"==typeof exports?module.exports=t():"function"==typeof define&&define.amd?define(t):"undefined"!=typeof window?window.vis=t():"undefined"!=typeof global?global.vis=t():"undefined"!=typeof self&&(self.vis=t())}(function(){var t;return function e(t,i,n){function s(r,a){if(!i[r]){if(!t[r]){var h="function"==typeof require&&require;if(!a&&h)return h(r,!0);if(o)return o(r,!0);throw new Error("Cannot find module '"+r+"'")}var d=i[r]={exports:{}};t[r][0].call(d.exports,function(e){var i=t[r][1][e];return s(i?i:e)},d,d.exports,e,t,i,n)}return i[r].exports}for(var o="function"==typeof require&&require,r=0;r0&&e==s.EVENT_END?e=s.EVENT_MOVE:l||(e=s.EVENT_END),l||null===o?o=h:h=o,i.call(s.detection,n.collectEventData(t,e,h)),s.HAS_POINTEREVENTS&&e==s.EVENT_END&&(l=s.PointerEvent.updatePointer(e,h))),l||(o=null,r=!1,a=!1,s.PointerEvent.reset())}})},determineEventTypes:function(){var t;t=s.HAS_POINTEREVENTS?s.PointerEvent.getEvents():s.NO_MOUSEEVENTS?["touchstart","touchmove","touchend touchcancel"]:["touchstart mousedown","touchmove mousemove","touchend touchcancel mouseup"],s.EVENT_TYPES[s.EVENT_START]=t[0],s.EVENT_TYPES[s.EVENT_MOVE]=t[1],s.EVENT_TYPES[s.EVENT_END]=t[2]},getTouchList:function(t){return s.HAS_POINTEREVENTS?s.PointerEvent.getTouchList():t.touches?t.touches:[{identifier:1,pageX:t.pageX,pageY:t.pageY,target:t.target}]},collectEventData:function(t,e,i){var n=this.getTouchList(i,e),o=s.POINTER_TOUCH;return(i.type.match(/mouse/)||s.PointerEvent.matchType(s.POINTER_MOUSE,i))&&(o=s.POINTER_MOUSE),{center:s.utils.getCenter(n),timeStamp:(new Date).getTime(),target:i.target,touches:n,eventType:e,pointerType:o,srcEvent:i,preventDefault:function(){this.srcEvent.preventManipulation&&this.srcEvent.preventManipulation(),this.srcEvent.preventDefault&&this.srcEvent.preventDefault()},stopPropagation:function(){this.srcEvent.stopPropagation()},stopDetect:function(){return s.detection.stopDetect()}}}},s.PointerEvent={pointers:{},getTouchList:function(){var t=this,e=[];return Object.keys(t.pointers).sort().forEach(function(i){e.push(t.pointers[i])}),e},updatePointer:function(t,e){return t==s.EVENT_END?this.pointers={}:(e.identifier=e.pointerId,this.pointers[e.pointerId]=e),Object.keys(this.pointers).length},matchType:function(t,e){if(!e.pointerType)return!1;var i={};return i[s.POINTER_MOUSE]=e.pointerType==e.MSPOINTER_TYPE_MOUSE||e.pointerType==s.POINTER_MOUSE,i[s.POINTER_TOUCH]=e.pointerType==e.MSPOINTER_TYPE_TOUCH||e.pointerType==s.POINTER_TOUCH,i[s.POINTER_PEN]=e.pointerType==e.MSPOINTER_TYPE_PEN||e.pointerType==s.POINTER_PEN,i[t]},getEvents:function(){return["pointerdown MSPointerDown","pointermove MSPointerMove","pointerup pointercancel MSPointerUp MSPointerCancel"]},reset:function(){this.pointers={}}},s.utils={extend:function(t,e,n){for(var s in e)t[s]!==i&&n||(t[s]=e[s]);return t},hasParent:function(t,e){for(;t;){if(t==e)return!0;t=t.parentNode}return!1},getCenter:function(t){for(var e=[],i=[],n=0,s=t.length;s>n;n++)e.push(t[n].pageX),i.push(t[n].pageY);return{pageX:(Math.min.apply(Math,e)+Math.max.apply(Math,e))/2,pageY:(Math.min.apply(Math,i)+Math.max.apply(Math,i))/2}},getVelocity:function(t,e,i){return{x:Math.abs(e/t)||0,y:Math.abs(i/t)||0}},getAngle:function(t,e){var i=e.pageY-t.pageY,n=e.pageX-t.pageX;return 180*Math.atan2(i,n)/Math.PI},getDirection:function(t,e){var i=Math.abs(t.pageX-e.pageX),n=Math.abs(t.pageY-e.pageY);return i>=n?t.pageX-e.pageX>0?s.DIRECTION_LEFT:s.DIRECTION_RIGHT:t.pageY-e.pageY>0?s.DIRECTION_UP:s.DIRECTION_DOWN},getDistance:function(t,e){var i=e.pageX-t.pageX,n=e.pageY-t.pageY;return Math.sqrt(i*i+n*n)},getScale:function(t,e){return t.length>=2&&e.length>=2?this.getDistance(e[0],e[1])/this.getDistance(t[0],t[1]):1},getRotation:function(t,e){return t.length>=2&&e.length>=2?this.getAngle(e[1],e[0])-this.getAngle(t[1],t[0]):0},isVertical:function(t){return t==s.DIRECTION_UP||t==s.DIRECTION_DOWN},stopDefaultBrowserBehavior:function(t,e){var i,n=["webkit","khtml","moz","ms","o",""];if(e&&t.style){for(var s=0;si;i++){var o=this.gestures[i];if(!this.stopped&&e[o.name]!==!1&&o.handler.call(o,t,this.current.inst)===!1){this.stopDetect();break}}return this.current&&(this.current.lastEvent=t),t.eventType==s.EVENT_END&&!t.touches.length-1&&this.stopDetect(),t}},stopDetect:function(){this.previous=s.utils.extend({},this.current),this.current=null,this.stopped=!0},extendEventData:function(t){var e=this.current.startEvent;if(e&&(t.touches.length!=e.touches.length||t.touches===e.touches)){e.touches=[];for(var i=0,n=t.touches.length;n>i;i++)e.touches.push(s.utils.extend({},t.touches[i]))}var o=t.timeStamp-e.timeStamp,r=t.center.pageX-e.center.pageX,a=t.center.pageY-e.center.pageY,h=s.utils.getVelocity(o,r,a);return s.utils.extend(t,{deltaTime:o,deltaX:r,deltaY:a,velocityX:h.x,velocityY:h.y,distance:s.utils.getDistance(e.center,t.center),angle:s.utils.getAngle(e.center,t.center),direction:s.utils.getDirection(e.center,t.center),scale:s.utils.getScale(e.touches,t.touches),rotation:s.utils.getRotation(e.touches,t.touches),startEvent:e}),t},register:function(t){var e=t.defaults||{};return e[t.name]===i&&(e[t.name]=!0),s.utils.extend(s.defaults,e,!0),t.index=t.index||1e3,this.gestures.push(t),this.gestures.sort(function(t,e){return t.indexe.index?1:0}),this.gestures}},s.gestures=s.gestures||{},s.gestures.Hold={name:"hold",index:10,defaults:{hold_timeout:500,hold_threshold:1},timer:null,handler:function(t,e){switch(t.eventType){case s.EVENT_START:clearTimeout(this.timer),s.detection.current.name=this.name,this.timer=setTimeout(function(){"hold"==s.detection.current.name&&e.trigger("hold",t)},e.options.hold_timeout);break;case s.EVENT_MOVE:t.distance>e.options.hold_threshold&&clearTimeout(this.timer);break;case s.EVENT_END:clearTimeout(this.timer)}}},s.gestures.Tap={name:"tap",index:100,defaults:{tap_max_touchtime:250,tap_max_distance:10,tap_always:!0,doubletap_distance:20,doubletap_interval:300},handler:function(t,e){if(t.eventType==s.EVENT_END){var i=s.detection.previous,n=!1;if(t.deltaTime>e.options.tap_max_touchtime||t.distance>e.options.tap_max_distance)return;i&&"tap"==i.name&&t.timeStamp-i.lastEvent.timeStamp0&&t.touches.length>e.options.swipe_max_touches)return;(t.velocityX>e.options.swipe_velocity||t.velocityY>e.options.swipe_velocity)&&(e.trigger(this.name,t),e.trigger(this.name+t.direction,t))}}},s.gestures.Drag={name:"drag",index:50,defaults:{drag_min_distance:10,drag_max_touches:1,drag_block_horizontal:!1,drag_block_vertical:!1,drag_lock_to_axis:!1,drag_lock_min_distance:25},triggered:!1,handler:function(t,e){if(s.detection.current.name!=this.name&&this.triggered)return e.trigger(this.name+"end",t),this.triggered=!1,void 0;if(!(e.options.drag_max_touches>0&&t.touches.length>e.options.drag_max_touches))switch(t.eventType){case s.EVENT_START:this.triggered=!1;break;case s.EVENT_MOVE:if(t.distancee.options.transform_min_rotation&&e.trigger("rotate",t),i>e.options.transform_min_scale&&(e.trigger("pinch",t),e.trigger("pinch"+(t.scale<1?"in":"out"),t));break;case s.EVENT_END:this.triggered&&e.trigger(this.name+"end",t),this.triggered=!1}}},s.gestures.Touch={name:"touch",index:-1/0,defaults:{prevent_default:!1,prevent_mouseevents:!1},handler:function(t,e){return e.options.prevent_mouseevents&&t.pointerType==s.POINTER_MOUSE?(t.stopDetect(),void 0):(e.options.prevent_default&&t.preventDefault(),t.eventType==s.EVENT_START&&e.trigger(this.name,t),void 0)}},s.gestures.Release={name:"release",index:1/0,handler:function(t,e){t.eventType==s.EVENT_END&&e.trigger(this.name,t)}},"object"==typeof e&&"object"==typeof e.exports?e.exports=s:(t.Hammer=s,"function"==typeof t.define&&t.define.amd&&t.define("hammer",[],function(){return s}))}(this)},{}],2:[function(e,i){(function(n){function s(t,e){return function(i){return p(t.call(this,i),e)}}function o(t,e){return function(i){return this.lang().ordinal(t.call(this,i),e)}}function r(){}function a(t){T(t),d(this,t)}function h(t){var e=v(t),i=e.year||0,n=e.month||0,s=e.week||0,o=e.day||0,r=e.hour||0,a=e.minute||0,h=e.second||0,d=e.millisecond||0;this._input=t,this._milliseconds=+d+1e3*h+6e4*a+36e5*r,this._days=+o+7*s,this._months=+n+12*i,this._data={},this._bubble()}function d(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return e.hasOwnProperty("toString")&&(t.toString=e.toString),e.hasOwnProperty("valueOf")&&(t.valueOf=e.valueOf),t}function l(t){return 0>t?Math.ceil(t):Math.floor(t)}function p(t,e){for(var i=t+"";i.lengthn;n++)(i&&t[n]!==e[n]||!i&&w(t[n])!==w(e[n]))&&r++;return r+o}function g(t){if(t){var e=t.toLowerCase().replace(/(.)s$/,"$1");t=ze[t]||He[e]||e}return t}function v(t){var e,i,n={};for(i in t)t.hasOwnProperty(i)&&(e=g(i),e&&(n[e]=t[i]));return n}function y(t){var e,i;if(0===t.indexOf("week"))e=7,i="day";else{if(0!==t.indexOf("month"))return;e=12,i="month"}se[t]=function(s,o){var r,a,h=se.fn._lang[t],d=[];if("number"==typeof s&&(o=s,s=n),a=function(t){var e=se().utc().set(i,t);return h.call(se.fn._lang,e,s||"")},null!=o)return a(o);for(r=0;e>r;r++)d.push(a(r));return d}}function w(t){var e=+t,i=0;return 0!==e&&isFinite(e)&&(i=e>=0?Math.floor(e):Math.ceil(e)),i}function _(t,e){return new Date(Date.UTC(t,e+1,0)).getUTCDate()}function b(t){return E(t)?366:365}function E(t){return 0===t%4&&0!==t%100||0===t%400}function T(t){var e;t._a&&-2===t._pf.overflow&&(e=t._a[de]<0||t._a[de]>11?de:t._a[le]<1||t._a[le]>_(t._a[he],t._a[de])?le:t._a[pe]<0||t._a[pe]>23?pe:t._a[ue]<0||t._a[ue]>59?ue:t._a[ce]<0||t._a[ce]>59?ce:t._a[fe]<0||t._a[fe]>999?fe:-1,t._pf._overflowDayOfYear&&(he>e||e>le)&&(e=le),t._pf.overflow=e)}function x(t){t._pf={empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1}}function S(t){return null==t._isValid&&(t._isValid=!isNaN(t._d.getTime())&&t._pf.overflow<0&&!t._pf.empty&&!t._pf.invalidMonth&&!t._pf.nullInput&&!t._pf.invalidFormat&&!t._pf.userInvalidated,t._strict&&(t._isValid=t._isValid&&0===t._pf.charsLeftOver&&0===t._pf.unusedTokens.length)),t._isValid}function M(t){return t?t.toLowerCase().replace("_","-"):t}function D(t,e){return e.abbr=t,me[t]||(me[t]=new r),me[t].set(e),me[t]}function C(t){delete me[t]}function O(t){var i,n,s,o,r=0,a=function(t){if(!me[t]&&ge)try{e("./lang/"+t)}catch(i){}return me[t]};if(!t)return se.fn._lang;if(!c(t)){if(n=a(t))return n;t=[t]}for(;r0;){if(n=a(o.slice(0,i).join("-")))return n;if(s&&s.length>=i&&m(o,s,!0)>=i-1)break;i--}r++}return se.fn._lang}function N(t){return t.match(/\[[\s\S]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function L(t){var e,i,n=t.match(_e);for(e=0,i=n.length;i>e;e++)n[e]=Ve[n[e]]?Ve[n[e]]:N(n[e]);return function(s){var o="";for(e=0;i>e;e++)o+=n[e]instanceof Function?n[e].call(s,t):n[e];return o}}function k(t,e){return t.isValid()?(e=I(e,t.lang()),Ue[e]||(Ue[e]=L(e)),Ue[e](t)):t.lang().invalidDate()}function I(t,e){function i(t){return e.longDateFormat(t)||t}var n=5;for(be.lastIndex=0;n>=0&&be.test(t);)t=t.replace(be,i),be.lastIndex=0,n-=1;return t}function A(t,e){var i;switch(t){case"DDDD":return xe;case"YYYY":case"GGGG":case"gggg":return Se;case"YYYYY":case"GGGGG":case"ggggg":return Me;case"S":case"SS":case"SSS":case"DDD":return Te;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return Ce;case"a":case"A":return O(e._l)._meridiemParse;case"X":return Le;case"Z":case"ZZ":return Oe;case"T":return Ne;case"SSSS":return De;case"MM":case"DD":case"YY":case"GG":case"gg":case"HH":case"hh":case"mm":case"ss":case"M":case"D":case"d":case"H":case"h":case"m":case"s":case"w":case"ww":case"W":case"WW":case"e":case"E":return Ee;default:return i=new RegExp(W(U(t.replace("\\","")),"i"))}}function P(t){var e=(Oe.exec(t)||[])[0],i=(e+"").match(Fe)||["-",0,0],n=+(60*i[1])+w(i[2]);return"+"===i[0]?-n:n}function F(t,e,i){var n,s=i._a;switch(t){case"M":case"MM":null!=e&&(s[de]=w(e)-1);break;case"MMM":case"MMMM":n=O(i._l).monthsParse(e),null!=n?s[de]=n:i._pf.invalidMonth=e;break;case"D":case"DD":null!=e&&(s[le]=w(e));break;case"DDD":case"DDDD":null!=e&&(i._dayOfYear=w(e));break;case"YY":s[he]=w(e)+(w(e)>68?1900:2e3);break;case"YYYY":case"YYYYY":s[he]=w(e);break;case"a":case"A":i._isPm=O(i._l).isPM(e);break;case"H":case"HH":case"h":case"hh":s[pe]=w(e);break;case"m":case"mm":s[ue]=w(e);break;case"s":case"ss":s[ce]=w(e);break;case"S":case"SS":case"SSS":case"SSSS":s[fe]=w(1e3*("0."+e));break;case"X":i._d=new Date(1e3*parseFloat(e));break;case"Z":case"ZZ":i._useUTC=!0,i._tzm=P(e);break;case"w":case"ww":case"W":case"WW":case"d":case"dd":case"ddd":case"dddd":case"e":case"E":t=t.substr(0,1);case"gg":case"gggg":case"GG":case"GGGG":case"GGGGG":t=t.substr(0,2),e&&(i._w=i._w||{},i._w[t]=e)}}function Y(t){var e,i,n,s,o,r,a,h,d,l,p=[];if(!t._d){for(n=z(t),t._w&&null==t._a[le]&&null==t._a[de]&&(o=function(e){return e?e.length<3?parseInt(e,10)>68?"19"+e:"20"+e:e:null==t._a[he]?se().weekYear():t._a[he]},r=t._w,null!=r.GG||null!=r.W||null!=r.E?a=J(o(r.GG),r.W||1,r.E,4,1):(h=O(t._l),d=null!=r.d?q(r.d,h):null!=r.e?parseInt(r.e,10)+h._week.dow:0,l=parseInt(r.w,10)||1,null!=r.d&&db(s)&&(t._pf._overflowDayOfYear=!0),i=X(s,0,t._dayOfYear),t._a[de]=i.getUTCMonth(),t._a[le]=i.getUTCDate()),e=0;3>e&&null==t._a[e];++e)t._a[e]=p[e]=n[e];for(;7>e;e++)t._a[e]=p[e]=null==t._a[e]?2===e?1:0:t._a[e];p[pe]+=w((t._tzm||0)/60),p[ue]+=w((t._tzm||0)%60),t._d=(t._useUTC?X:B).apply(null,p)}}function R(t){var e;t._d||(e=v(t._i),t._a=[e.year,e.month,e.day,e.hour,e.minute,e.second,e.millisecond],Y(t))}function z(t){var e=new Date;return t._useUTC?[e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()]:[e.getFullYear(),e.getMonth(),e.getDate()]}function H(t){t._a=[],t._pf.empty=!0;var e,i,n,s,o,r=O(t._l),a=""+t._i,h=a.length,d=0;for(n=I(t._f,r).match(_e)||[],e=0;e0&&t._pf.unusedInput.push(o),a=a.slice(a.indexOf(i)+i.length),d+=i.length),Ve[s]?(i?t._pf.empty=!1:t._pf.unusedTokens.push(s),F(s,i,t)):t._strict&&!i&&t._pf.unusedTokens.push(s);t._pf.charsLeftOver=h-d,a.length>0&&t._pf.unusedInput.push(a),t._isPm&&t._a[pe]<12&&(t._a[pe]+=12),t._isPm===!1&&12===t._a[pe]&&(t._a[pe]=0),Y(t),T(t)}function U(t){return t.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(t,e,i,n,s){return e||i||n||s})}function W(t){return t.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function j(t){var e,i,n,s,o;if(0===t._f.length)return t._pf.invalidFormat=!0,t._d=new Date(0/0),void 0;for(s=0;so)&&(n=o,i=e));d(t,i||e)}function V(t){var e,i=t._i,n=ke.exec(i);if(n){for(t._pf.iso=!0,e=4;e>0;e--)if(n[e]){t._f=Ae[e-1]+(n[6]||" ");break}for(e=0;4>e;e++)if(Pe[e][1].exec(i)){t._f+=Pe[e][0];break}Oe.exec(i)&&(t._f+="Z"),H(t)}else t._d=new Date(i)}function G(t){var e=t._i,i=ve.exec(e);e===n?t._d=new Date:i?t._d=new Date(+i[1]):"string"==typeof e?V(t):c(e)?(t._a=e.slice(0),Y(t)):f(e)?t._d=new Date(+e):"object"==typeof e?R(t):t._d=new Date(e)}function B(t,e,i,n,s,o,r){var a=new Date(t,e,i,n,s,o,r);return 1970>t&&a.setFullYear(t),a}function X(t){var e=new Date(Date.UTC.apply(null,arguments));return 1970>t&&e.setUTCFullYear(t),e}function q(t,e){if("string"==typeof t)if(isNaN(t)){if(t=e.weekdaysParse(t),"number"!=typeof t)return null}else t=parseInt(t,10);return t}function Z(t,e,i,n,s){return s.relativeTime(e||1,!!i,t,n)}function K(t,e,i){var n=ae(Math.abs(t)/1e3),s=ae(n/60),o=ae(s/60),r=ae(o/24),a=ae(r/365),h=45>n&&["s",n]||1===s&&["m"]||45>s&&["mm",s]||1===o&&["h"]||22>o&&["hh",o]||1===r&&["d"]||25>=r&&["dd",r]||45>=r&&["M"]||345>r&&["MM",ae(r/30)]||1===a&&["y"]||["yy",a];return h[2]=e,h[3]=t>0,h[4]=i,Z.apply({},h)}function $(t,e,i){var n,s=i-e,o=i-t.day();return o>s&&(o-=7),s-7>o&&(o+=7),n=se(t).add("d",o),{week:Math.ceil(n.dayOfYear()/7),year:n.year()}}function J(t,e,i,n,s){var o,r,a=new Date(Date.UTC(t,0)).getUTCDay();return i=null!=i?i:s,o=s-a+(a>n?7:0),r=7*(e-1)+(i-s)+o+1,{year:r>0?t:t-1,dayOfYear:r>0?r:b(t-1)+r}}function Q(t){var e=t._i,i=t._f;return"undefined"==typeof t._pf&&x(t),null===e?se.invalid({nullInput:!0}):("string"==typeof e&&(t._i=e=O().preparse(e)),se.isMoment(e)?(t=d({},e),t._d=new Date(+e._d)):i?c(i)?j(t):H(t):G(t),new a(t))}function te(t,e){se.fn[t]=se.fn[t+"s"]=function(t){var i=this._isUTC?"UTC":"";return null!=t?(this._d["set"+i+e](t),se.updateOffset(this),this):this._d["get"+i+e]()}}function ee(t){se.duration.fn[t]=function(){return this._data[t]}}function ie(t,e){se.duration.fn["as"+t]=function(){return+this/e}}function ne(t){var e=!1,i=se;"undefined"==typeof ender&&(this.moment=t?function(){return!e&&console&&console.warn&&(e=!0,console.warn("Accessing Moment through the global scope is deprecated, and will be removed in an upcoming release.")),i.apply(null,arguments)}:se)}for(var se,oe,re="2.4.0",ae=Math.round,he=0,de=1,le=2,pe=3,ue=4,ce=5,fe=6,me={},ge="undefined"!=typeof i&&i.exports,ve=/^\/?Date\((\-?\d+)/i,ye=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,we=/^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,_e=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,be=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,Ee=/\d\d?/,Te=/\d{1,3}/,xe=/\d{3}/,Se=/\d{1,4}/,Me=/[+\-]?\d{1,6}/,De=/\d+/,Ce=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,Oe=/Z|[\+\-]\d\d:?\d\d/i,Ne=/T/i,Le=/[\+\-]?\d+(\.\d{1,3})?/,ke=/^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d:?\d\d|Z)?)?$/,Ie="YYYY-MM-DDTHH:mm:ssZ",Ae=["YYYY-MM-DD","GGGG-[W]WW","GGGG-[W]WW-E","YYYY-DDD"],Pe=[["HH:mm:ss.SSSS",/(T| )\d\d:\d\d:\d\d\.\d{1,3}/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],Fe=/([\+\-]|\d\d)/gi,Ye="Date|Hours|Minutes|Seconds|Milliseconds".split("|"),Re={Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6},ze={ms:"millisecond",s:"second",m:"minute",h:"hour",d:"day",D:"date",w:"week",W:"isoWeek",M:"month",y:"year",DDD:"dayOfYear",e:"weekday",E:"isoWeekday",gg:"weekYear",GG:"isoWeekYear"},He={dayofyear:"dayOfYear",isoweekday:"isoWeekday",isoweek:"isoWeek",weekyear:"weekYear",isoweekyear:"isoWeekYear"},Ue={},We="DDD w W M D d".split(" "),je="M D H h m s w W".split(" "),Ve={M:function(){return this.month()+1},MMM:function(t){return this.lang().monthsShort(this,t)},MMMM:function(t){return this.lang().months(this,t)},D:function(){return this.date()},DDD:function(){return this.dayOfYear()},d:function(){return this.day()},dd:function(t){return this.lang().weekdaysMin(this,t)},ddd:function(t){return this.lang().weekdaysShort(this,t)},dddd:function(t){return this.lang().weekdays(this,t)},w:function(){return this.week()},W:function(){return this.isoWeek()},YY:function(){return p(this.year()%100,2)},YYYY:function(){return p(this.year(),4)},YYYYY:function(){return p(this.year(),5)},gg:function(){return p(this.weekYear()%100,2)},gggg:function(){return this.weekYear()},ggggg:function(){return p(this.weekYear(),5)},GG:function(){return p(this.isoWeekYear()%100,2)},GGGG:function(){return this.isoWeekYear()},GGGGG:function(){return p(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return w(this.milliseconds()/100)},SS:function(){return p(w(this.milliseconds()/10),2)},SSS:function(){return p(this.milliseconds(),3)},SSSS:function(){return p(this.milliseconds(),3)},Z:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+p(w(t/60),2)+":"+p(w(t)%60,2)},ZZ:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+p(w(10*t/6),4)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()}},Ge=["months","monthsShort","weekdays","weekdaysShort","weekdaysMin"];We.length;)oe=We.pop(),Ve[oe+"o"]=o(Ve[oe],oe);for(;je.length;)oe=je.pop(),Ve[oe+oe]=s(Ve[oe],2);for(Ve.DDDD=s(Ve.DDD,3),d(r.prototype,{set:function(t){var e,i;for(i in t)e=t[i],"function"==typeof e?this[i]=e:this["_"+i]=e},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(t){return this._months[t.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(t){return this._monthsShort[t.month()]},monthsParse:function(t){var e,i,n;for(this._monthsParse||(this._monthsParse=[]),e=0;12>e;e++)if(this._monthsParse[e]||(i=se.utc([2e3,e]),n="^"+this.months(i,"")+"|^"+this.monthsShort(i,""),this._monthsParse[e]=new RegExp(n.replace(".",""),"i")),this._monthsParse[e].test(t))return e},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(t){return this._weekdays[t.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(t){return this._weekdaysShort[t.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(t){return this._weekdaysMin[t.day()]},weekdaysParse:function(t){var e,i,n;for(this._weekdaysParse||(this._weekdaysParse=[]),e=0;7>e;e++)if(this._weekdaysParse[e]||(i=se([2e3,1]).day(e),n="^"+this.weekdays(i,"")+"|^"+this.weekdaysShort(i,"")+"|^"+this.weekdaysMin(i,""),this._weekdaysParse[e]=new RegExp(n.replace(".",""),"i")),this._weekdaysParse[e].test(t))return e},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(t){var e=this._longDateFormat[t];return!e&&this._longDateFormat[t.toUpperCase()]&&(e=this._longDateFormat[t.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t]=e),e},isPM:function(t){return"p"===(t+"").toLowerCase().charAt(0)},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(t,e,i){return t>11?i?"pm":"PM":i?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(t,e){var i=this._calendar[t];return"function"==typeof i?i.apply(e):i},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(t,e,i,n){var s=this._relativeTime[i];return"function"==typeof s?s(t,e,i,n):s.replace(/%d/i,t)},pastFuture:function(t,e){var i=this._relativeTime[t>0?"future":"past"];return"function"==typeof i?i(e):i.replace(/%s/i,e)},ordinal:function(t){return this._ordinal.replace("%d",t)},_ordinal:"%d",preparse:function(t){return t},postformat:function(t){return t},week:function(t){return $(t,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6},_invalidDate:"Invalid date",invalidDate:function(){return this._invalidDate}}),se=function(t,e,i,s){return"boolean"==typeof i&&(s=i,i=n),Q({_i:t,_f:e,_l:i,_strict:s,_isUTC:!1})},se.utc=function(t,e,i,s){var o;return"boolean"==typeof i&&(s=i,i=n),o=Q({_useUTC:!0,_isUTC:!0,_l:i,_i:t,_f:e,_strict:s}).utc()},se.unix=function(t){return se(1e3*t)},se.duration=function(t,e){var i,n,s,o=se.isDuration(t),r="number"==typeof t,a=o?t._input:r?{}:t,d=null;return r?e?a[e]=t:a.milliseconds=t:(d=ye.exec(t))?(i="-"===d[1]?-1:1,a={y:0,d:w(d[le])*i,h:w(d[pe])*i,m:w(d[ue])*i,s:w(d[ce])*i,ms:w(d[fe])*i}):(d=we.exec(t))&&(i="-"===d[1]?-1:1,s=function(t){var e=t&&parseFloat(t.replace(",","."));return(isNaN(e)?0:e)*i},a={y:s(d[2]),M:s(d[3]),d:s(d[4]),h:s(d[5]),m:s(d[6]),s:s(d[7]),w:s(d[8])}),n=new h(a),o&&t.hasOwnProperty("_lang")&&(n._lang=t._lang),n},se.version=re,se.defaultFormat=Ie,se.updateOffset=function(){},se.lang=function(t,e){var i;return t?(e?D(M(t),e):null===e?(C(t),t="en"):me[t]||O(t),i=se.duration.fn._lang=se.fn._lang=O(t),i._abbr):se.fn._lang._abbr},se.langData=function(t){return t&&t._lang&&t._lang._abbr&&(t=t._lang._abbr),O(t)},se.isMoment=function(t){return t instanceof a},se.isDuration=function(t){return t instanceof h},oe=Ge.length-1;oe>=0;--oe)y(Ge[oe]);for(se.normalizeUnits=function(t){return g(t)},se.invalid=function(t){var e=se.utc(0/0);return null!=t?d(e._pf,t):e._pf.userInvalidated=!0,e},se.parseZone=function(t){return se(t).parseZone()},d(se.fn=a.prototype,{clone:function(){return se(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.clone().lang("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){return k(se(this).utc(),"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]")},toArray:function(){var t=this;return[t.year(),t.month(),t.date(),t.hours(),t.minutes(),t.seconds(),t.milliseconds()]},isValid:function(){return S(this)},isDSTShifted:function(){return this._a?this.isValid()&&m(this._a,(this._isUTC?se.utc(this._a):se(this._a)).toArray())>0:!1},parsingFlags:function(){return d({},this._pf)},invalidAt:function(){return this._pf.overflow},utc:function(){return this.zone(0)},local:function(){return this.zone(0),this._isUTC=!1,this},format:function(t){var e=k(this,t||se.defaultFormat);return this.lang().postformat(e)},add:function(t,e){var i;return i="string"==typeof t?se.duration(+e,t):se.duration(t,e),u(this,i,1),this},subtract:function(t,e){var i;return i="string"==typeof t?se.duration(+e,t):se.duration(t,e),u(this,i,-1),this},diff:function(t,e,i){var n,s,o=this._isUTC?se(t).zone(this._offset||0):se(t).local(),r=6e4*(this.zone()-o.zone());return e=g(e),"year"===e||"month"===e?(n=432e5*(this.daysInMonth()+o.daysInMonth()),s=12*(this.year()-o.year())+(this.month()-o.month()),s+=(this-se(this).startOf("month")-(o-se(o).startOf("month")))/n,s-=6e4*(this.zone()-se(this).startOf("month").zone()-(o.zone()-se(o).startOf("month").zone()))/n,"year"===e&&(s/=12)):(n=this-o,s="second"===e?n/1e3:"minute"===e?n/6e4:"hour"===e?n/36e5:"day"===e?(n-r)/864e5:"week"===e?(n-r)/6048e5:n),i?s:l(s) -},from:function(t,e){return se.duration(this.diff(t)).lang(this.lang()._abbr).humanize(!e)},fromNow:function(t){return this.from(se(),t)},calendar:function(){var t=this.diff(se().zone(this.zone()).startOf("day"),"days",!0),e=-6>t?"sameElse":-1>t?"lastWeek":0>t?"lastDay":1>t?"sameDay":2>t?"nextDay":7>t?"nextWeek":"sameElse";return this.format(this.lang().calendar(e,this))},isLeapYear:function(){return E(this.year())},isDST:function(){return this.zone()+se(t).startOf(e)},isBefore:function(t,e){return e="undefined"!=typeof e?e:"millisecond",+this.clone().startOf(e)<+se(t).startOf(e)},isSame:function(t,e){return e="undefined"!=typeof e?e:"millisecond",+this.clone().startOf(e)===+se(t).startOf(e)},min:function(t){return t=se.apply(null,arguments),this>t?this:t},max:function(t){return t=se.apply(null,arguments),t>this?this:t},zone:function(t){var e=this._offset||0;return null==t?this._isUTC?e:this._d.getTimezoneOffset():("string"==typeof t&&(t=P(t)),Math.abs(t)<16&&(t=60*t),this._offset=t,this._isUTC=!0,e!==t&&u(this,se.duration(e-t,"m"),1,!0),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},parseZone:function(){return"string"==typeof this._i&&this.zone(this._i),this},hasAlignedHourOffset:function(t){return t=t?se(t).zone():0,0===(this.zone()-t)%60},daysInMonth:function(){return _(this.year(),this.month())},dayOfYear:function(t){var e=ae((se(this).startOf("day")-se(this).startOf("year"))/864e5)+1;return null==t?e:this.add("d",t-e)},weekYear:function(t){var e=$(this,this.lang()._week.dow,this.lang()._week.doy).year;return null==t?e:this.add("y",t-e)},isoWeekYear:function(t){var e=$(this,1,4).year;return null==t?e:this.add("y",t-e)},week:function(t){var e=this.lang().week(this);return null==t?e:this.add("d",7*(t-e))},isoWeek:function(t){var e=$(this,1,4).week;return null==t?e:this.add("d",7*(t-e))},weekday:function(t){var e=(this.day()+7-this.lang()._week.dow)%7;return null==t?e:this.add("d",t-e)},isoWeekday:function(t){return null==t?this.day()||7:this.day(this.day()%7?t:t-7)},get:function(t){return t=g(t),this[t]()},set:function(t,e){return t=g(t),"function"==typeof this[t]&&this[t](e),this},lang:function(t){return t===n?this._lang:(this._lang=O(t),this)}}),oe=0;oei;++i)t.call(e||this,this[i],i,this)}),Array.prototype.map||(Array.prototype.map=function(t,e){var i,n,s;if(null==this)throw new TypeError(" this is null or not defined");var o=Object(this),r=o.length>>>0;if("function"!=typeof t)throw new TypeError(t+" is not a function");for(e&&(i=e),n=new Array(r),s=0;r>s;){var a,h;s in o&&(a=o[s],h=t.call(i,a,s,o),n[s]=h),s++}return n}),Array.prototype.filter||(Array.prototype.filter=function(t){"use strict";if(null==this)throw new TypeError;var e=Object(this),i=e.length>>>0;if("function"!=typeof t)throw new TypeError;for(var n=[],s=arguments[1],o=0;i>o;o++)if(o in e){var r=e[o];t.call(s,r,o,e)&&n.push(r)}return n}),Object.keys||(Object.keys=function(){var t=Object.prototype.hasOwnProperty,e=!{toString:null}.propertyIsEnumerable("toString"),i=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],n=i.length;return function(s){if("object"!=typeof s&&"function"!=typeof s||null===s)throw new TypeError("Object.keys called on non-object");var o=[];for(var r in s)t.call(s,r)&&o.push(r);if(e)for(var a=0;n>a;a++)t.call(s,i[a])&&o.push(i[a]);return o}}()),Array.isArray||(Array.isArray=function(t){return"[object Array]"===Object.prototype.toString.call(t)}),Function.prototype.bind||(Function.prototype.bind=function(t){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var e=Array.prototype.slice.call(arguments,1),i=this,n=function(){},s=function(){return i.apply(this instanceof n&&t?this:t,e.concat(Array.prototype.slice.call(arguments)))};return n.prototype=this.prototype,s.prototype=new n,s}),Object.create||(Object.create=function(t){function e(){}if(arguments.length>1)throw new Error("Object.create implementation only accepts the first parameter.");return e.prototype=t,new e}),Function.prototype.bind||(Function.prototype.bind=function(t){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var e=Array.prototype.slice.call(arguments,1),i=this,n=function(){},s=function(){return i.apply(this instanceof n&&t?this:t,e.concat(Array.prototype.slice.call(arguments)))};return n.prototype=this.prototype,s.prototype=new n,s});var k={};k.isNumber=function(t){return t instanceof Number||"number"==typeof t},k.isString=function(t){return t instanceof String||"string"==typeof t},k.isDate=function(t){if(t instanceof Date)return!0;if(k.isString(t)){var e=I.exec(t);if(e)return!0;if(!isNaN(Date.parse(t)))return!0}return!1},k.isDataTable=function(t){return"undefined"!=typeof google&&google.visualization&&google.visualization.DataTable&&t instanceof google.visualization.DataTable},k.randomUUID=function(){var t=function(){return Math.floor(65536*Math.random()).toString(16)};return t()+t()+"-"+t()+"-"+t()+"-"+t()+"-"+t()+t()+t()},k.extend=function(t){for(var e=1,i=arguments.length;i>e;e++){var n=arguments[e];for(var s in n)n.hasOwnProperty(s)&&void 0!==n[s]&&(t[s]=n[s])}return t},k.convert=function(t,e){var i;if(void 0===t)return void 0;if(null===t)return null;if(!e)return t;if("string"!=typeof e&&!(e instanceof String))throw new Error("Type must be a string");switch(e){case"boolean":case"Boolean":return Boolean(t);case"number":case"Number":return Number(t.valueOf());case"string":case"String":return String(t);case"Date":if(k.isNumber(t))return new Date(t);if(t instanceof Date)return new Date(t.valueOf());if(N.isMoment(t))return new Date(t.valueOf());if(k.isString(t))return i=I.exec(t),i?new Date(Number(i[1])):N(t).toDate();throw new Error("Cannot convert object of type "+k.getType(t)+" to type Date");case"Moment":if(k.isNumber(t))return N(t);if(t instanceof Date)return N(t.valueOf());if(N.isMoment(t))return N(t);if(k.isString(t))return i=I.exec(t),i?N(Number(i[1])):N(t);throw new Error("Cannot convert object of type "+k.getType(t)+" to type Date");case"ISODate":if(k.isNumber(t))return new Date(t);if(t instanceof Date)return t.toISOString();if(N.isMoment(t))return t.toDate().toISOString();if(k.isString(t))return i=I.exec(t),i?new Date(Number(i[1])).toISOString():new Date(t).toISOString();throw new Error("Cannot convert object of type "+k.getType(t)+" to type ISODate");case"ASPDate":if(k.isNumber(t))return"/Date("+t+")/";if(t instanceof Date)return"/Date("+t.valueOf()+")/";if(k.isString(t)){i=I.exec(t);var n;return n=i?new Date(Number(i[1])).valueOf():new Date(t).valueOf(),"/Date("+n+")/"}throw new Error("Cannot convert object of type "+k.getType(t)+" to type ASPDate");default:throw new Error("Cannot convert object of type "+k.getType(t)+' to type "'+e+'"')}};var I=/^\/?Date\((\-?\d+)/i;k.getType=function(t){var e=typeof t;return"object"==e?null==t?"null":t instanceof Boolean?"Boolean":t instanceof Number?"Number":t instanceof String?"String":t instanceof Array?"Array":t instanceof Date?"Date":"Object":"number"==e?"Number":"boolean"==e?"Boolean":"string"==e?"String":e},k.getAbsoluteLeft=function(t){for(var e=document.documentElement,i=document.body,n=t.offsetLeft,s=t.offsetParent;null!=s&&s!=i&&s!=e;)n+=s.offsetLeft,n-=s.scrollLeft,s=s.offsetParent;return n},k.getAbsoluteTop=function(t){for(var e=document.documentElement,i=document.body,n=t.offsetTop,s=t.offsetParent;null!=s&&s!=i&&s!=e;)n+=s.offsetTop,n-=s.scrollTop,s=s.offsetParent;return n},k.getPageY=function(t){if("pageY"in t)return t.pageY;var e;e="targetTouches"in t&&t.targetTouches.length?t.targetTouches[0].clientY:t.clientY;var i=document.documentElement,n=document.body;return e+(i&&i.scrollTop||n&&n.scrollTop||0)-(i&&i.clientTop||n&&n.clientTop||0)},k.getPageX=function(t){if("pageY"in t)return t.pageX;var e;e="targetTouches"in t&&t.targetTouches.length?t.targetTouches[0].clientX:t.clientX;var i=document.documentElement,n=document.body;return e+(i&&i.scrollLeft||n&&n.scrollLeft||0)-(i&&i.clientLeft||n&&n.clientLeft||0)},k.addClassName=function(t,e){var i=t.className.split(" ");-1==i.indexOf(e)&&(i.push(e),t.className=i.join(" "))},k.removeClassName=function(t,e){var i=t.className.split(" "),n=i.indexOf(e);-1!=n&&(i.splice(n,1),t.className=i.join(" "))},k.forEach=function(t,e){var i,n;if(t instanceof Array)for(i=0,n=t.length;n>i;i++)e(t[i],i,t);else for(i in t)t.hasOwnProperty(i)&&e(t[i],i,t)},k.updateProperty=function(t,e,i){return t[e]!==i?(t[e]=i,!0):!1},k.addEventListener=function(t,e,i,n){t.addEventListener?(void 0===n&&(n=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.addEventListener(e,i,n)):t.attachEvent("on"+e,i)},k.removeEventListener=function(t,e,i,n){t.removeEventListener?(void 0===n&&(n=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.removeEventListener(e,i,n)):t.detachEvent("on"+e,i)},k.getTarget=function(t){t||(t=window.event);var e;return t.target?e=t.target:t.srcElement&&(e=t.srcElement),void 0!=e.nodeType&&3==e.nodeType&&(e=e.parentNode),e},k.stopPropagation=function(t){t||(t=window.event),t.stopPropagation?t.stopPropagation():t.cancelBubble=!0},k.preventDefault=function(t){t||(t=window.event),t.preventDefault?t.preventDefault():t.returnValue=!1},k.option={},k.option.asBoolean=function(t,e){return"function"==typeof t&&(t=t()),null!=t?0!=t:e||null},k.option.asNumber=function(t,e){return"function"==typeof t&&(t=t()),null!=t?Number(t)||e||null:e||null},k.option.asString=function(t,e){return"function"==typeof t&&(t=t()),null!=t?String(t):e||null},k.option.asSize=function(t,e){return"function"==typeof t&&(t=t()),k.isString(t)?t:k.isNumber(t)?t+"px":e||null},k.option.asElement=function(t,e){return"function"==typeof t&&(t=t()),t||e||null},k.loadCss=function(t){if("undefined"!=typeof document){var e=document.createElement("style");e.type="text/css",e.styleSheet?e.styleSheet.cssText=t:e.appendChild(document.createTextNode(t)),document.getElementsByTagName("head")[0].appendChild(e)}};var A={listeners:[],indexOf:function(t){for(var e=this.listeners,i=0,n=this.listeners.length;n>i;i++){var s=e[i];if(s&&s.object==t)return i}return-1},addListener:function(t,e,i){var n=this.indexOf(t),s=this.listeners[n];s||(s={object:t,events:{}},this.listeners.push(s));var o=s.events[e];o||(o=[],s.events[e]=o),-1==o.indexOf(i)&&o.push(i)},removeListener:function(t,e,i){var n=this.indexOf(t),s=this.listeners[n];if(s){var o=s.events[e];o&&(n=o.indexOf(i),-1!=n&&o.splice(n,1),0==o.length&&delete s.events[e]);var r=0,a=s.events;for(var h in a)a.hasOwnProperty(h)&&r++;0==r&&delete this.listeners[n]}},removeAllListeners:function(){this.listeners=[]},trigger:function(t,e,i){var n=this.indexOf(t),s=this.listeners[n];if(s){var o=s.events[e];if(o)for(var r=0,a=o.length;a>r;r++)o[r](i)}}};s.prototype.on=function(t,e,i){var n=t instanceof RegExp?t:new RegExp(t.replace("*","\\w+")),s={id:k.randomUUID(),event:t,regexp:n,callback:"function"==typeof e?e:null,target:i};return this.subscriptions.push(s),s.id},s.prototype.off=function(t){for(var e=0;eo;o++)i=s._addItem(t[o]),n.push(i);else if(k.isDataTable(t))for(var a=this._getColumnNames(t),h=0,d=t.getNumberOfRows();d>h;h++){for(var l={},p=0,u=a.length;u>p;p++){var c=a[p];l[c]=t.getValue(h,p)}i=s._addItem(l),n.push(i)}else{if(!(t instanceof Object))throw new Error("Unknown dataType");i=s._addItem(t),n.push(i)}return n.length&&this._trigger("add",{items:n},e),n},o.prototype.update=function(t,e){var i=[],n=[],s=this,o=s.fieldId,r=function(t){var e=t[o];s.data[e]?(e=s._updateItem(t),n.push(e)):(e=s._addItem(t),i.push(e))};if(t instanceof Array)for(var a=0,h=t.length;h>a;a++)r(t[a]);else if(k.isDataTable(t))for(var d=this._getColumnNames(t),l=0,p=t.getNumberOfRows();p>l;l++){for(var u={},c=0,f=d.length;f>c;c++){var m=d[c];u[m]=t.getValue(l,c)}r(u)}else{if(!(t instanceof Object))throw new Error("Unknown dataType");r(t)}return i.length&&this._trigger("add",{items:i},e),n.length&&this._trigger("update",{items:n},e),i.concat(n)},o.prototype.get=function(){var t,e,i,n,s=this,o=k.getType(arguments[0]);"String"==o||"Number"==o?(t=arguments[0],i=arguments[1],n=arguments[2]):"Array"==o?(e=arguments[0],i=arguments[1],n=arguments[2]):(i=arguments[0],n=arguments[1]);var r;if(i&&i.type){if(r="DataTable"==i.type?"DataTable":"Array",n&&r!=k.getType(n))throw new Error('Type of parameter "data" ('+k.getType(n)+") "+"does not correspond with specified options.type ("+i.type+")");if("DataTable"==r&&!k.isDataTable(n))throw new Error('Parameter "data" must be a DataTable when options.type is "DataTable"')}else r=n?"DataTable"==k.getType(n)?"DataTable":"Array":"Array";var a,h,d,l,p=i&&i.convert||this.options.convert,u=i&&i.filter,c=[];if(void 0!=t)a=s._getItem(t,p),u&&!u(a)&&(a=null);else if(void 0!=e)for(d=0,l=e.length;l>d;d++)a=s._getItem(e[d],p),(!u||u(a))&&c.push(a);else for(h in this.data)this.data.hasOwnProperty(h)&&(a=s._getItem(h,p),(!u||u(a))&&c.push(a));if(i&&i.order&&void 0==t&&this._sort(c,i.order),i&&i.fields){var f=i.fields;if(void 0!=t)a=this._filterFields(a,f);else for(d=0,l=c.length;l>d;d++)c[d]=this._filterFields(c[d],f)}if("DataTable"==r){var m=this._getColumnNames(n);if(void 0!=t)s._appendRow(n,m,a);else for(d=0,l=c.length;l>d;d++)s._appendRow(n,m,c[d]);return n}if(void 0!=t)return a;if(n){for(d=0,l=c.length;l>d;d++)n.push(c[d]);return n}return c},o.prototype.getIds=function(t){var e,i,n,s,o,r=this.data,a=t&&t.filter,h=t&&t.order,d=t&&t.convert||this.options.convert,l=[];if(a)if(h){o=[];for(n in r)r.hasOwnProperty(n)&&(s=this._getItem(n,d),a(s)&&o.push(s));for(this._sort(o,h),e=0,i=o.length;i>e;e++)l[e]=o[e][this.fieldId]}else for(n in r)r.hasOwnProperty(n)&&(s=this._getItem(n,d),a(s)&&l.push(s[this.fieldId]));else if(h){o=[];for(n in r)r.hasOwnProperty(n)&&o.push(r[n]);for(this._sort(o,h),e=0,i=o.length;i>e;e++)l[e]=o[e][this.fieldId]}else for(n in r)r.hasOwnProperty(n)&&(s=r[n],l.push(s[this.fieldId]));return l},o.prototype.forEach=function(t,e){var i,n,s=e&&e.filter,o=e&&e.convert||this.options.convert,r=this.data;if(e&&e.order)for(var a=this.get(e),h=0,d=a.length;d>h;h++)i=a[h],n=i[this.fieldId],t(i,n);else for(n in r)r.hasOwnProperty(n)&&(i=this._getItem(n,o),(!s||s(i))&&t(i,n))},o.prototype.map=function(t,e){var i,n=e&&e.filter,s=e&&e.convert||this.options.convert,o=[],r=this.data;for(var a in r)r.hasOwnProperty(a)&&(i=this._getItem(a,s),(!n||n(i))&&o.push(t(i,a)));return e&&e.order&&this._sort(o,e.order),o},o.prototype._filterFields=function(t,e){var i={};for(var n in t)t.hasOwnProperty(n)&&-1!=e.indexOf(n)&&(i[n]=t[n]);return i},o.prototype._sort=function(t,e){if(k.isString(e)){var i=e;t.sort(function(t,e){var n=t[i],s=e[i];return n>s?1:s>n?-1:0})}else{if("function"!=typeof e)throw new TypeError("Order must be a function or a string");t.sort(e)}},o.prototype.remove=function(t,e){var i,n,s,o=[];if(t instanceof Array)for(i=0,n=t.length;n>i;i++)s=this._remove(t[i]),null!=s&&o.push(s);else s=this._remove(t),null!=s&&o.push(s);return o.length&&this._trigger("remove",{items:o},e),o},o.prototype._remove=function(t){if(k.isNumber(t)||k.isString(t)){if(this.data[t])return delete this.data[t],delete this.internalIds[t],t}else if(t instanceof Object){var e=t[this.fieldId];if(e&&this.data[e])return delete this.data[e],delete this.internalIds[e],e}return null},o.prototype.clear=function(t){var e=Object.keys(this.data);return this.data={},this.internalIds={},this._trigger("remove",{items:e},t),e},o.prototype.max=function(t){var e=this.data,i=null,n=null;for(var s in e)if(e.hasOwnProperty(s)){var o=e[s],r=o[t];null!=r&&(!i||r>n)&&(i=o,n=r)}return i},o.prototype.min=function(t){var e=this.data,i=null,n=null;for(var s in e)if(e.hasOwnProperty(s)){var o=e[s],r=o[t];null!=r&&(!i||n>r)&&(i=o,n=r)}return i},o.prototype.distinct=function(t){var e=this.data,i=[],n=this.options.convert[t],s=0;for(var o in e)if(e.hasOwnProperty(o)){for(var r=e[o],a=k.convert(r[t],n),h=!1,d=0;s>d;d++)if(i[d]==a){h=!0;break}h||(i[s]=a,s++)}return i},o.prototype._addItem=function(t){var e=t[this.fieldId];if(void 0!=e){if(this.data[e])throw new Error("Cannot add item: item with id "+e+" already exists")}else e=k.randomUUID(),t[this.fieldId]=e,this.internalIds[e]=t;var i={};for(var n in t)if(t.hasOwnProperty(n)){var s=this.convert[n];i[n]=k.convert(t[n],s)}return this.data[e]=i,e},o.prototype._getItem=function(t,e){var i,n,s=this.data[t];if(!s)return null;var o={},r=this.fieldId,a=this.internalIds;if(e)for(i in s)s.hasOwnProperty(i)&&(n=s[i],i==r&&n in a||(o[i]=k.convert(n,e[i])));else for(i in s)s.hasOwnProperty(i)&&(n=s[i],i==r&&n in a||(o[i]=n));return o},o.prototype._updateItem=function(t){var e=t[this.fieldId];if(void 0==e)throw new Error("Cannot update item: item has no id (item: "+JSON.stringify(t)+")");var i=this.data[e];if(!i)throw new Error("Cannot update item: no item with id "+e+" found");for(var n in t)if(t.hasOwnProperty(n)){var s=this.convert[n];i[n]=k.convert(t[n],s)}return e},o.prototype._getColumnNames=function(t){for(var e=[],i=0,n=t.getNumberOfColumns();n>i;i++)e[i]=t.getColumnId(i)||t.getColumnLabel(i);return e},o.prototype._appendRow=function(t,e,i){for(var n=t.addRow(),s=0,o=e.length;o>s;s++){var r=e[s];t.setValue(n,s,i[r])}},r.prototype.setData=function(t){var e,i,n;if(this.data){this.data.unsubscribe&&this.data.unsubscribe("*",this.listener),e=[];for(var s in this.ids)this.ids.hasOwnProperty(s)&&e.push(s);this.ids={},this._trigger("remove",{items:e})}if(this.data=t,this.data){for(this.fieldId=this.options.fieldId||this.data&&this.data.options&&this.data.options.fieldId||"id",e=this.data.getIds({filter:this.options&&this.options.filter}),i=0,n=e.length;n>i;i++)s=e[i],this.ids[s]=!0;this._trigger("add",{items:e}),this.data.subscribe&&this.data.subscribe("*",this.listener)}},r.prototype.get=function(){var t,e,i,n=this,s=k.getType(arguments[0]);"String"==s||"Number"==s||"Array"==s?(t=arguments[0],e=arguments[1],i=arguments[2]):(e=arguments[0],i=arguments[1]);var o=k.extend({},this.options,e);this.options.filter&&e&&e.filter&&(o.filter=function(t){return n.options.filter(t)&&e.filter(t)});var r=[];return void 0!=t&&r.push(t),r.push(o),r.push(i),this.data&&this.data.get.apply(this.data,r)},r.prototype.getIds=function(t){var e;if(this.data){var i,n=this.options.filter;i=t&&t.filter?n?function(e){return n(e)&&t.filter(e)}:t.filter:n,e=this.data.getIds({filter:i,order:t&&t.order})}else e=[];return e},r.prototype._onEvent=function(t,e,i){var n,s,o,r,a=e&&e.items,h=this.data,d=[],l=[],p=[]; -if(a&&h){switch(t){case"add":for(n=0,s=a.length;s>n;n++)o=a[n],r=this.get(o),r&&(this.ids[o]=!0,d.push(o));break;case"update":for(n=0,s=a.length;s>n;n++)o=a[n],r=this.get(o),r?this.ids[o]?l.push(o):(this.ids[o]=!0,d.push(o)):this.ids[o]&&(delete this.ids[o],p.push(o));break;case"remove":for(n=0,s=a.length;s>n;n++)o=a[n],this.ids[o]&&(delete this.ids[o],p.push(o))}d.length&&this._trigger("add",{items:d},i),l.length&&this._trigger("update",{items:l},i),p.length&&this._trigger("remove",{items:p},i)}},r.prototype.subscribe=o.prototype.subscribe,r.prototype.unsubscribe=o.prototype.unsubscribe,r.prototype._trigger=o.prototype._trigger,TimeStep=function(t,e,i){this.current=new Date,this._start=new Date,this._end=new Date,this.autoScale=!0,this.scale=TimeStep.SCALE.DAY,this.step=1,this.setRange(t,e,i)},TimeStep.SCALE={MILLISECOND:1,SECOND:2,MINUTE:3,HOUR:4,DAY:5,WEEKDAY:6,MONTH:7,YEAR:8},TimeStep.prototype.setRange=function(t,e,i){if(!(t instanceof Date&&e instanceof Date))throw"No legal start or end date in method setRange";this._start=void 0!=t?new Date(t.valueOf()):new Date,this._end=void 0!=e?new Date(e.valueOf()):new Date,this.autoScale&&this.setMinimumStep(i)},TimeStep.prototype.first=function(){this.current=new Date(this._start.valueOf()),this.roundToMinor()},TimeStep.prototype.roundToMinor=function(){switch(this.scale){case TimeStep.SCALE.YEAR:this.current.setFullYear(this.step*Math.floor(this.current.getFullYear()/this.step)),this.current.setMonth(0);case TimeStep.SCALE.MONTH:this.current.setDate(1);case TimeStep.SCALE.DAY:case TimeStep.SCALE.WEEKDAY:this.current.setHours(0);case TimeStep.SCALE.HOUR:this.current.setMinutes(0);case TimeStep.SCALE.MINUTE:this.current.setSeconds(0);case TimeStep.SCALE.SECOND:this.current.setMilliseconds(0)}if(1!=this.step)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current.setMilliseconds(this.current.getMilliseconds()-this.current.getMilliseconds()%this.step);break;case TimeStep.SCALE.SECOND:this.current.setSeconds(this.current.getSeconds()-this.current.getSeconds()%this.step);break;case TimeStep.SCALE.MINUTE:this.current.setMinutes(this.current.getMinutes()-this.current.getMinutes()%this.step);break;case TimeStep.SCALE.HOUR:this.current.setHours(this.current.getHours()-this.current.getHours()%this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()-1-(this.current.getDate()-1)%this.step+1);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()-this.current.getMonth()%this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()-this.current.getFullYear()%this.step)}},TimeStep.prototype.hasNext=function(){return this.current.valueOf()<=this._end.valueOf()},TimeStep.prototype.next=function(){var t=this.current.valueOf();if(this.current.getMonth()<6)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current=new Date(this.current.valueOf()+this.step);break;case TimeStep.SCALE.SECOND:this.current=new Date(this.current.valueOf()+1e3*this.step);break;case TimeStep.SCALE.MINUTE:this.current=new Date(this.current.valueOf()+60*1e3*this.step);break;case TimeStep.SCALE.HOUR:this.current=new Date(this.current.valueOf()+60*60*1e3*this.step);var e=this.current.getHours();this.current.setHours(e-e%this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()+this.step);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()+this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()+this.step)}else switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current=new Date(this.current.valueOf()+this.step);break;case TimeStep.SCALE.SECOND:this.current.setSeconds(this.current.getSeconds()+this.step);break;case TimeStep.SCALE.MINUTE:this.current.setMinutes(this.current.getMinutes()+this.step);break;case TimeStep.SCALE.HOUR:this.current.setHours(this.current.getHours()+this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()+this.step);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()+this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()+this.step)}if(1!=this.step)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current.getMilliseconds()0&&(this.step=e),this.autoScale=!1},TimeStep.prototype.setAutoScale=function(t){this.autoScale=t},TimeStep.prototype.setMinimumStep=function(t){if(void 0!=t){var e=31104e6,i=2592e6,n=864e5,s=36e5,o=6e4,r=1e3,a=1;1e3*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1e3),500*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=500),100*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=100),50*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=50),10*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=10),5*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=5),e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1),3*i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=3),i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=1),5*n>t&&(this.scale=TimeStep.SCALE.DAY,this.step=5),2*n>t&&(this.scale=TimeStep.SCALE.DAY,this.step=2),n>t&&(this.scale=TimeStep.SCALE.DAY,this.step=1),n/2>t&&(this.scale=TimeStep.SCALE.WEEKDAY,this.step=1),4*s>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=4),s>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=1),15*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=15),10*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=10),5*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=5),o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=1),15*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=15),10*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=10),5*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=5),r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=1),200*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=200),100*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=100),50*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=50),10*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=10),5*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=5),a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=1)}},TimeStep.prototype.snap=function(t){if(this.scale==TimeStep.SCALE.YEAR){var e=t.getFullYear()+Math.round(t.getMonth()/12);t.setFullYear(Math.round(e/this.step)*this.step),t.setMonth(0),t.setDate(0),t.setHours(0),t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.MONTH)t.getDate()>15?(t.setDate(1),t.setMonth(t.getMonth()+1)):t.setDate(1),t.setHours(0),t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0);else if(this.scale==TimeStep.SCALE.DAY||this.scale==TimeStep.SCALE.WEEKDAY){switch(this.step){case 5:case 2:t.setHours(24*Math.round(t.getHours()/24));break;default:t.setHours(12*Math.round(t.getHours()/12))}t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.HOUR){switch(this.step){case 4:t.setMinutes(60*Math.round(t.getMinutes()/60));break;default:t.setMinutes(30*Math.round(t.getMinutes()/30))}t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.MINUTE){switch(this.step){case 15:case 10:t.setMinutes(5*Math.round(t.getMinutes()/5)),t.setSeconds(0);break;case 5:t.setSeconds(60*Math.round(t.getSeconds()/60));break;default:t.setSeconds(30*Math.round(t.getSeconds()/30))}t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.SECOND)switch(this.step){case 15:case 10:t.setSeconds(5*Math.round(t.getSeconds()/5)),t.setMilliseconds(0);break;case 5:t.setMilliseconds(1e3*Math.round(t.getMilliseconds()/1e3));break;default:t.setMilliseconds(500*Math.round(t.getMilliseconds()/500))}else if(this.scale==TimeStep.SCALE.MILLISECOND){var i=this.step>5?this.step/2:1;t.setMilliseconds(Math.round(t.getMilliseconds()/i)*i)}},TimeStep.prototype.isMajor=function(){switch(this.scale){case TimeStep.SCALE.MILLISECOND:return 0==this.current.getMilliseconds();case TimeStep.SCALE.SECOND:return 0==this.current.getSeconds();case TimeStep.SCALE.MINUTE:return 0==this.current.getHours()&&0==this.current.getMinutes();case TimeStep.SCALE.HOUR:return 0==this.current.getHours();case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:return 1==this.current.getDate();case TimeStep.SCALE.MONTH:return 0==this.current.getMonth();case TimeStep.SCALE.YEAR:return!1;default:return!1}},TimeStep.prototype.getLabelMinor=function(t){switch(void 0==t&&(t=this.current),this.scale){case TimeStep.SCALE.MILLISECOND:return N(t).format("SSS");case TimeStep.SCALE.SECOND:return N(t).format("s");case TimeStep.SCALE.MINUTE:return N(t).format("HH:mm");case TimeStep.SCALE.HOUR:return N(t).format("HH:mm");case TimeStep.SCALE.WEEKDAY:return N(t).format("ddd D");case TimeStep.SCALE.DAY:return N(t).format("D");case TimeStep.SCALE.MONTH:return N(t).format("MMM");case TimeStep.SCALE.YEAR:return N(t).format("YYYY");default:return""}},TimeStep.prototype.getLabelMajor=function(t){switch(void 0==t&&(t=this.current),this.scale){case TimeStep.SCALE.MILLISECOND:return N(t).format("HH:mm:ss");case TimeStep.SCALE.SECOND:return N(t).format("D MMMM HH:mm");case TimeStep.SCALE.MINUTE:case TimeStep.SCALE.HOUR:return N(t).format("ddd D MMMM");case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:return N(t).format("MMMM YYYY");case TimeStep.SCALE.MONTH:return N(t).format("YYYY");case TimeStep.SCALE.YEAR:return"";default:return""}},a.prototype.setOptions=function(t){k.extend(this.options,t)},a.prototype.update=function(){this._order(),this._stack()},a.prototype._order=function(){var t=this.parent.items;if(!t)throw new Error("Cannot stack items: parent does not contain items");var e=[],i=0;k.forEach(t,function(t){t.visible&&(e[i]=t,i++)});var n=this.options.order||this.defaultOptions.order;if("function"!=typeof n)throw new Error("Option order must be a function");e.sort(n),this.ordered=e},a.prototype._stack=function(){var t,e,i,n=this.ordered,s=this.options,o=s.orientation||this.defaultOptions.orientation,r="top"==o;for(i=s.margin&&void 0!==s.margin.item?s.margin.item:this.defaultOptions.margin.item,t=0,e=n.length;e>t;t++){var a=n[t],h=null;do h=this.checkOverlap(n,t,0,t-1,i),null!=h&&(a.top=r?h.top+h.height+i:h.top-a.height-i);while(h)}},a.prototype.checkOverlap=function(t,e,i,n,s){for(var o=this.collision,r=t[e],a=n;a>=i;a--){var h=t[a];if(o(r,h,s)&&a!=e)return h}return null},a.prototype.collision=function(t,e,i){return t.left-ie.left&&t.top-ie.top},h.prototype.setOptions=function(t){k.extend(this.options,t),null!==this.start&&null!==this.end&&this.setRange(this.start,this.end)},h.prototype.subscribe=function(t,e,i){var n,s=this;if("horizontal"!=i&&"vertical"!=i)throw new TypeError('Unknown direction "'+i+'". '+'Choose "horizontal" or "vertical".');if("move"==e)n={component:t,event:e,direction:i,callback:function(t){s._onMouseDown(t,n)},params:{}},t.on("mousedown",n.callback),s.listeners.push(n);else{if("zoom"!=e)throw new TypeError('Unknown event "'+e+'". '+'Choose "move" or "zoom".');n={component:t,event:e,direction:i,callback:function(t){s._onMouseWheel(t,n)},params:{}},t.on("mousewheel",n.callback),s.listeners.push(n)}},h.prototype.on=function(t,e){A.addListener(this,t,e)},h.prototype._trigger=function(t){A.trigger(this,t,{start:this.start,end:this.end})},h.prototype.setRange=function(t,e){var i=this._applyRange(t,e);i&&(this._trigger("rangechange"),this._trigger("rangechanged"))},h.prototype._applyRange=function(t,e){var i,n=null!=t?k.convert(t,"Number"):this.start,s=null!=e?k.convert(e,"Number"):this.end,o=null!=this.options.max?k.convert(this.options.max,"Date").valueOf():null,r=null!=this.options.min?k.convert(this.options.min,"Date").valueOf():null;if(isNaN(n)||null===n)throw new Error('Invalid start "'+t+'"');if(isNaN(s)||null===s)throw new Error('Invalid end "'+e+'"');if(n>s&&(s=n),null!==r&&r>n&&(i=r-n,n+=i,s+=i,null!=o&&s>o&&(s=o)),null!==o&&s>o&&(i=s-o,n-=i,s-=i,null!=r&&r>n&&(n=r)),null!==this.options.zoomMin){var a=parseFloat(this.options.zoomMin);0>a&&(a=0),a>s-n&&(this.end-this.start===a?(n=this.start,s=this.end):(i=a-(s-n),n-=i/2,s+=i/2))}if(null!==this.options.zoomMax){var h=parseFloat(this.options.zoomMax);0>h&&(h=0),s-n>h&&(this.end-this.start===h?(n=this.start,s=this.end):(i=s-n-h,n+=i/2,s-=i/2))}var d=this.start!=n||this.end!=s;return this.start=n,this.end=s,d},h.prototype.getRange=function(){return{start:this.start,end:this.end}},h.prototype.conversion=function(t){return this.start,this.end,h.conversion(this.start,this.end,t)},h.conversion=function(t,e,i){return 0!=i&&0!=e-t?{offset:t,factor:i/(e-t)}:{offset:0,factor:1}},h.prototype._onMouseDown=function(t,e){t=t||window.event;var i=e.params,n=t.which?1==t.which:1==t.button;if(n){i.mouseX=k.getPageX(t),i.mouseY=k.getPageY(t),i.previousLeft=0,i.previousOffset=0,i.moved=!1,i.start=this.start,i.end=this.end;var s=e.component.frame;s&&(s.style.cursor="move");var o=this;i.onMouseMove||(i.onMouseMove=function(t){o._onMouseMove(t,e)},k.addEventListener(document,"mousemove",i.onMouseMove)),i.onMouseUp||(i.onMouseUp=function(t){o._onMouseUp(t,e)},k.addEventListener(document,"mouseup",i.onMouseUp)),k.preventDefault(t)}},h.prototype._onMouseMove=function(t,e){t=t||window.event;var i=e.params,n=k.getPageX(t),s=k.getPageY(t);void 0==i.mouseX&&(i.mouseX=n),void 0==i.mouseY&&(i.mouseY=s);var o=n-i.mouseX,r=s-i.mouseY,a="horizontal"==e.direction?o:r;Math.abs(a)>=1&&(i.moved=!0);var h=i.end-i.start,d="horizontal"==e.direction?e.component.width:e.component.height,l=-a/d*h;this._applyRange(i.start+l,i.end+l),this._trigger("rangechange"),k.preventDefault(t)},h.prototype._onMouseUp=function(t,e){t=t||window.event;var i=e.params;e.component.frame&&(e.component.frame.style.cursor="auto"),i.onMouseMove&&(k.removeEventListener(document,"mousemove",i.onMouseMove),i.onMouseMove=null),i.onMouseUp&&(k.removeEventListener(document,"mouseup",i.onMouseUp),i.onMouseUp=null),i.moved&&this._trigger("rangechanged")},h.prototype._onMouseWheel=function(t,e){t=t||window.event;var i=0;if(t.wheelDelta?i=t.wheelDelta/120:t.detail&&(i=-t.detail/3),i){var n=this,s=function(){var s=i/5,o=null,r=e.component.frame;if(r){var a,h;if("horizontal"==e.direction){a=e.component.width,h=n.conversion(a);var d=k.getAbsoluteLeft(r),l=k.getPageX(t);o=(l-d)/h.factor+h.offset}else{a=e.component.height,h=n.conversion(a);var p=k.getAbsoluteTop(r),u=k.getPageY(t);o=(p+a-u-p)/h.factor+h.offset}}n.zoom(s,o)};s()}k.preventDefault(t)},h.prototype.zoom=function(t,e){null==e&&(e=(this.start+this.end)/2),t>=1&&(t=.9),-1>=t&&(t=-.9),0>t&&(t/=1+t);var i=this.start-e,n=this.end-e,s=this.start-i*t,o=this.end-n*t;this.setRange(s,o)},h.prototype.move=function(t){var e=this.end-this.start,i=this.start+e*t,n=this.end+e*t;this.start=i,this.end=n},h.prototype.moveTo=function(t){var e=(this.start+this.end)/2,i=e-t,n=this.start-i,s=this.end-i;this.setRange(n,s)},d.prototype.add=function(t){if(void 0==t.id)throw new Error("Component has no field id");if(!(t instanceof l||t instanceof d))throw new TypeError("Component must be an instance of prototype Component or Controller");t.controller=this,this.components[t.id]=t},d.prototype.remove=function(t){var e;for(e in this.components)if(this.components.hasOwnProperty(e)&&(e==t||this.components[e]==t))break;e&&delete this.components[e]},d.prototype.requestReflow=function(t){if(t)this.reflow();else if(!this.reflowTimer){var e=this;this.reflowTimer=setTimeout(function(){e.reflowTimer=void 0,e.reflow()},0)}},d.prototype.requestRepaint=function(t){if(t)this.repaint();else if(!this.repaintTimer){var e=this;this.repaintTimer=setTimeout(function(){e.repaintTimer=void 0,e.repaint()},0)}},d.prototype.repaint=function F(){function F(i,n){n in e||(i.depends&&i.depends.forEach(function(t){F(t,t.id)}),i.parent&&F(i.parent,i.parent.id),t=i.repaint()||t,e[n]=!0)}var t=!1;this.repaintTimer&&(clearTimeout(this.repaintTimer),this.repaintTimer=void 0);var e={};k.forEach(this.components,F),t&&this.reflow()},d.prototype.reflow=function Y(){function Y(i,n){n in e||(i.depends&&i.depends.forEach(function(t){Y(t,t.id)}),i.parent&&Y(i.parent,i.parent.id),t=i.reflow()||t,e[n]=!0)}var t=!1;this.reflowTimer&&(clearTimeout(this.reflowTimer),this.reflowTimer=void 0);var e={};k.forEach(this.components,Y),t&&this.repaint()},l.prototype.setOptions=function(t){t&&(k.extend(this.options,t),this.controller&&(this.requestRepaint(),this.requestReflow()))},l.prototype.getOption=function(t){var e;return this.options&&(e=this.options[t]),void 0===e&&this.defaultOptions&&(e=this.defaultOptions[t]),e},l.prototype.getContainer=function(){return null},l.prototype.getFrame=function(){return this.frame},l.prototype.repaint=function(){return!1},l.prototype.reflow=function(){return!1},l.prototype.hide=function(){return this.frame&&this.frame.parentNode?(this.frame.parentNode.removeChild(this.frame),!0):!1},l.prototype.show=function(){return this.frame&&this.frame.parentNode?!1:this.repaint()},l.prototype.requestRepaint=function(){if(!this.controller)throw new Error("Cannot request a repaint: no controller configured");this.controller.requestRepaint()},l.prototype.requestReflow=function(){if(!this.controller)throw new Error("Cannot request a reflow: no controller configured");this.controller.requestReflow()},p.prototype=new l,p.prototype.setOptions=l.prototype.setOptions,p.prototype.getContainer=function(){return this.frame},p.prototype.repaint=function(){var t=0,e=k.updateProperty,i=k.option.asSize,n=this.options,s=this.frame;if(!s){s=document.createElement("div"),s.className="panel";var o=n.className;o&&("function"==typeof o?k.addClassName(s,String(o())):k.addClassName(s,String(o))),this.frame=s,t+=1}if(!s.parentNode){if(!this.parent)throw new Error("Cannot repaint panel: no parent attached");var r=this.parent.getContainer();if(!r)throw new Error("Cannot repaint panel: parent has no container element");r.appendChild(s),t+=1}return t+=e(s.style,"top",i(n.top,"0px")),t+=e(s.style,"left",i(n.left,"0px")),t+=e(s.style,"width",i(n.width,"100%")),t+=e(s.style,"height",i(n.height,"100%")),t>0},p.prototype.reflow=function(){var t=0,e=k.updateProperty,i=this.frame;return i?(t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft),t+=e(this,"width",i.offsetWidth),t+=e(this,"height",i.offsetHeight)):t+=1,t>0},u.prototype=new p,u.prototype.setOptions=l.prototype.setOptions,u.prototype.repaint=function(){var t=0,e=k.updateProperty,i=k.option.asSize,n=this.options,s=this.frame;if(!s){s=document.createElement("div"),s.className="vis timeline rootpanel";var o=n.className;o&&k.addClassName(s,k.option.asString(o)),this.frame=s,t+=1}if(!s.parentNode){if(!this.container)throw new Error("Cannot repaint root panel: no container attached");this.container.appendChild(s),t+=1}return t+=e(s.style,"top",i(n.top,"0px")),t+=e(s.style,"left",i(n.left,"0px")),t+=e(s.style,"width",i(n.width,"100%")),t+=e(s.style,"height",i(n.height,"100%")),this._updateEventEmitters(),this._updateWatch(),t>0},u.prototype.reflow=function(){var t=0,e=k.updateProperty,i=this.frame;return i?(t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft),t+=e(this,"width",i.offsetWidth),t+=e(this,"height",i.offsetHeight)):t+=1,t>0},u.prototype._updateWatch=function(){var t=this.getOption("autoResize");t?this._watch():this._unwatch()},u.prototype._watch=function(){var t=this;this._unwatch();var e=function(){var e=t.getOption("autoResize");return e?(t.frame&&(t.frame.clientWidth!=t.width||t.frame.clientHeight!=t.height)&&t.requestReflow(),void 0):(t._unwatch(),void 0)};k.addEventListener(window,"resize",e),this.watchTimer=setInterval(e,1e3)},u.prototype._unwatch=function(){this.watchTimer&&(clearInterval(this.watchTimer),this.watchTimer=void 0)},u.prototype.on=function(t,e){var i=this.listeners[t];i||(i=[],this.listeners[t]=i),i.push(e),this._updateEventEmitters()},u.prototype._updateEventEmitters=function(){if(this.listeners){var t=this;k.forEach(this.listeners,function(e,i){if(t.emitters||(t.emitters={}),!(i in t.emitters)){var n=t.frame;if(n){var s=function(t){e.forEach(function(e){e(t)})};t.emitters[i]=s,k.addEventListener(n,i,s)}}})}},c.prototype=new l,c.prototype.setOptions=l.prototype.setOptions,c.prototype.setRange=function(t){if(!(t instanceof h||t&&t.start&&t.end))throw new TypeError("Range must be an instance of Range, or an object containing start and end.");this.range=t},c.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.factor+e.offset)},c.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.factor},c.prototype.repaint=function(){var t=0,e=k.updateProperty,i=k.option.asSize,n=this.options,s=this.getOption("orientation"),o=this.props,r=this.step,a=this.frame;if(a||(a=document.createElement("div"),this.frame=a,t+=1),a.className="axis "+s,!a.parentNode){if(!this.parent)throw new Error("Cannot repaint time axis: no parent attached");var h=this.parent.getContainer();if(!h)throw new Error("Cannot repaint time axis: parent has no container element");h.appendChild(a),t+=1}var d=a.parentNode;if(d){var l=a.nextSibling;d.removeChild(a);var p="bottom"==s&&this.props.parentHeight&&this.height?this.props.parentHeight-this.height+"px":"0px";if(t+=e(a.style,"top",i(n.top,p)),t+=e(a.style,"left",i(n.left,"0px")),t+=e(a.style,"width",i(n.width,"100%")),t+=e(a.style,"height",i(n.height,this.height+"px")),this._repaintMeasureChars(),this.step){this._repaintStart(),r.first();for(var u=void 0,c=0;r.hasNext()&&1e3>c;){c++;var f=r.getCurrent(),m=this.toScreen(f),g=r.isMajor();this.getOption("showMinorLabels")&&this._repaintMinorText(m,r.getLabelMinor()),g&&this.getOption("showMajorLabels")?(m>0&&(void 0==u&&(u=m),this._repaintMajorText(m,r.getLabelMajor())),this._repaintMajorLine(m)):this._repaintMinorLine(m),r.next()}if(this.getOption("showMajorLabels")){var v=this.toTime(0),y=r.getLabelMajor(v),w=y.length*(o.majorCharWidth||10)+10;(void 0==u||u>w)&&this._repaintMajorText(0,y)}this._repaintEnd()}this._repaintLine(),l?d.insertBefore(a,l):d.appendChild(a)}return t>0},c.prototype._repaintStart=function(){var t=this.dom,e=t.redundant;e.majorLines=t.majorLines,e.majorTexts=t.majorTexts,e.minorLines=t.minorLines,e.minorTexts=t.minorTexts,t.majorLines=[],t.majorTexts=[],t.minorLines=[],t.minorTexts=[]},c.prototype._repaintEnd=function(){k.forEach(this.dom.redundant,function(t){for(;t.length;){var e=t.pop();e&&e.parentNode&&e.parentNode.removeChild(e)}})},c.prototype._repaintMinorText=function(t,e){var i=this.dom.redundant.minorTexts.shift();if(!i){var n=document.createTextNode("");i=document.createElement("div"),i.appendChild(n),i.className="text minor",this.frame.appendChild(i)}this.dom.minorTexts.push(i),i.childNodes[0].nodeValue=e,i.style.left=t+"px",i.style.top=this.props.minorLabelTop+"px"},c.prototype._repaintMajorText=function(t,e){var i=this.dom.redundant.majorTexts.shift();if(!i){var n=document.createTextNode(e);i=document.createElement("div"),i.className="text major",i.appendChild(n),this.frame.appendChild(i)}this.dom.majorTexts.push(i),i.childNodes[0].nodeValue=e,i.style.top=this.props.majorLabelTop+"px",i.style.left=t+"px"},c.prototype._repaintMinorLine=function(t){var e=this.dom.redundant.minorLines.shift();e||(e=document.createElement("div"),e.className="grid vertical minor",this.frame.appendChild(e)),this.dom.minorLines.push(e);var i=this.props;e.style.top=i.minorLineTop+"px",e.style.height=i.minorLineHeight+"px",e.style.left=t-i.minorLineWidth/2+"px"},c.prototype._repaintMajorLine=function(t){var e=this.dom.redundant.majorLines.shift();e||(e=document.createElement("DIV"),e.className="grid vertical major",this.frame.appendChild(e)),this.dom.majorLines.push(e);var i=this.props;e.style.top=i.majorLineTop+"px",e.style.left=t-i.majorLineWidth/2+"px",e.style.height=i.majorLineHeight+"px"},c.prototype._repaintLine=function(){var t=this.dom.line,e=this.frame;this.options,this.getOption("showMinorLabels")||this.getOption("showMajorLabels")?(t?(e.removeChild(t),e.appendChild(t)):(t=document.createElement("div"),t.className="grid horizontal major",e.appendChild(t),this.dom.line=t),t.style.top=this.props.lineTop+"px"):t&&axis.parentElement&&(e.removeChild(axis.line),delete this.dom.line)},c.prototype._repaintMeasureChars=function(){var t,e=this.dom;if(!e.measureCharMinor){t=document.createTextNode("0");var i=document.createElement("DIV");i.className="text minor measure",i.appendChild(t),this.frame.appendChild(i),e.measureCharMinor=i}if(!e.measureCharMajor){t=document.createTextNode("0");var n=document.createElement("DIV");n.className="text major measure",n.appendChild(t),this.frame.appendChild(n),e.measureCharMajor=n}},c.prototype.reflow=function(){var t=0,e=k.updateProperty,i=this.frame,n=this.range;if(!n)throw new Error("Cannot repaint time axis: no range configured");if(i){t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft);var s=this.props,o=this.getOption("showMinorLabels"),r=this.getOption("showMajorLabels"),a=this.dom.measureCharMinor,h=this.dom.measureCharMajor;a&&(s.minorCharHeight=a.clientHeight,s.minorCharWidth=a.clientWidth),h&&(s.majorCharHeight=h.clientHeight,s.majorCharWidth=h.clientWidth);var d=i.parentNode?i.parentNode.offsetHeight:0;switch(d!=s.parentHeight&&(s.parentHeight=d,t+=1),this.getOption("orientation")){case"bottom":s.minorLabelHeight=o?s.minorCharHeight:0,s.majorLabelHeight=r?s.majorCharHeight:0,s.minorLabelTop=0,s.majorLabelTop=s.minorLabelTop+s.minorLabelHeight,s.minorLineTop=-this.top,s.minorLineHeight=Math.max(this.top+s.majorLabelHeight,0),s.minorLineWidth=1,s.majorLineTop=-this.top,s.majorLineHeight=Math.max(this.top+s.minorLabelHeight+s.majorLabelHeight,0),s.majorLineWidth=1,s.lineTop=0;break;case"top":s.minorLabelHeight=o?s.minorCharHeight:0,s.majorLabelHeight=r?s.majorCharHeight:0,s.majorLabelTop=0,s.minorLabelTop=s.majorLabelTop+s.majorLabelHeight,s.minorLineTop=s.minorLabelTop,s.minorLineHeight=Math.max(d-s.majorLabelHeight-this.top),s.minorLineWidth=1,s.majorLineTop=0,s.majorLineHeight=Math.max(d-this.top),s.majorLineWidth=1,s.lineTop=s.majorLabelHeight+s.minorLabelHeight;break;default:throw new Error('Unkown orientation "'+this.getOption("orientation")+'"')}var l=s.minorLabelHeight+s.majorLabelHeight;t+=e(this,"width",i.offsetWidth),t+=e(this,"height",l),this._updateConversion();var p=k.convert(n.start,"Number"),u=k.convert(n.end,"Number"),c=this.toTime(5*(s.minorCharWidth||10)).valueOf()-this.toTime(0).valueOf();this.step=new TimeStep(new Date(p),new Date(u),c),t+=e(s.range,"start",p),t+=e(s.range,"end",u),t+=e(s.range,"minimumStep",c.valueOf())}return t>0},c.prototype._updateConversion=function(){var t=this.range;if(!t)throw new Error("No range configured");this.conversion=t.conversion?t.conversion(this.width):h.conversion(t.start,t.end,this.width)},f.prototype=new l,f.prototype.setOptions=l.prototype.setOptions,f.prototype.getContainer=function(){return this.frame},f.prototype.repaint=function(){var t=this.frame,e=this.parent,i=e.parent.getContainer();if(!e)throw new Error("Cannot repaint bar: no parent attached");if(!i)throw new Error("Cannot repaint bar: parent has no container element");if(!this.getOption("showCurrentTime"))return t&&(i.removeChild(t),delete this.frame),void 0;t||(t=document.createElement("div"),t.className="currenttime",t.style.position="absolute",t.style.top="0px",t.style.height="100%",i.appendChild(t),this.frame=t),e.conversion||e._updateConversion();var n=new Date,s=e.toScreen(n);t.style.left=s+"px",t.title="Current time: "+n,void 0!==this.currentTimeTimer&&(clearTimeout(this.currentTimeTimer),delete this.currentTimeTimer);var o=this,r=1/e.conversion.factor/2;return 30>r&&(r=30),this.currentTimeTimer=setTimeout(function(){o.repaint()},r),!1},m.prototype=new l,m.prototype.setOptions=l.prototype.setOptions,m.prototype.getContainer=function(){return this.frame},m.prototype.repaint=function(){var t=this.frame,e=this.parent,i=e.parent.getContainer();if(!e)throw new Error("Cannot repaint bar: no parent attached");if(!i)throw new Error("Cannot repaint bar: parent has no container element");if(!this.getOption("showCustomTime"))return t&&(i.removeChild(t),delete this.frame),void 0;if(!t){t=document.createElement("div"),t.className="customtime",t.style.position="absolute",t.style.top="0px",t.style.height="100%",i.appendChild(t);var n=document.createElement("div");n.style.position="relative",n.style.top="0px",n.style.left="-10px",n.style.height="100%",n.style.width="20px",t.appendChild(n),this.frame=t,this.subscribe(this,"movetime")}e.conversion||e._updateConversion();var s=e.toScreen(this.customTime);return t.style.left=s+"px",t.title="Time: "+this.customTime,!1},m.prototype._setCustomTime=function(t){this.customTime=new Date(t.valueOf()),this.repaint()},m.prototype._getCustomTime=function(){return new Date(this.customTime.valueOf())},m.prototype.subscribe=function(t,e){var i=this,n={component:t,event:e,callback:function(t){i._onMouseDown(t,n)},params:{}};t.on("mousedown",n.callback),i.listeners.push(n)},m.prototype.on=function(t,e){var i=this.frame;if(!i)throw new Error("Cannot add event listener: no parent attached");A.addListener(this,t,e),k.addEventListener(i,t,e)},m.prototype._onMouseDown=function(t,e){t=t||window.event;var i=e.params,n=t.which?1==t.which:1==t.button;if(n){i.mouseX=k.getPageX(t),i.moved=!1,i.customTime=this.customTime;var s=this;i.onMouseMove||(i.onMouseMove=function(t){s._onMouseMove(t,e)},k.addEventListener(document,"mousemove",i.onMouseMove)),i.onMouseUp||(i.onMouseUp=function(t){s._onMouseUp(t,e)},k.addEventListener(document,"mouseup",i.onMouseUp)),k.stopPropagation(t),k.preventDefault(t)}},m.prototype._onMouseMove=function(t,e){t=t||window.event;var i=e.params,n=this.parent,s=k.getPageX(t);void 0===i.mouseX&&(i.mouseX=s);var o=s-i.mouseX;Math.abs(o)>=1&&(i.moved=!0);var r=n.toScreen(i.customTime),a=r+o,h=n.toTime(a);this._setCustomTime(h),A.trigger(this,"timechange",{customTime:this.customTime}),k.preventDefault(t)},m.prototype._onMouseUp=function(t,e){t=t||window.event;var i=e.params;i.onMouseMove&&(k.removeEventListener(document,"mousemove",i.onMouseMove),i.onMouseMove=null),i.onMouseUp&&(k.removeEventListener(document,"mouseup",i.onMouseUp),i.onMouseUp=null),i.moved&&A.trigger(this,"timechanged",{customTime:this.customTime})},g.prototype=new p,g.types={box:y,range:_,rangeoverflow:b,point:w},g.prototype.setOptions=l.prototype.setOptions,g.prototype.setRange=function(t){if(!(t instanceof h||t&&t.start&&t.end))throw new TypeError("Range must be an instance of Range, or an object containing start and end.");this.range=t},g.prototype.repaint=function(){var t=0,e=k.updateProperty,i=k.option.asSize,n=this.options,s=this.getOption("orientation"),o=this.defaultOptions,r=this.frame;if(!r){r=document.createElement("div"),r.className="itemset";var a=n.className;a&&k.addClassName(r,k.option.asString(a));var h=document.createElement("div");h.className="background",r.appendChild(h),this.dom.background=h;var d=document.createElement("div");d.className="foreground",r.appendChild(d),this.dom.foreground=d;var l=document.createElement("div");l.className="itemset-axis",this.dom.axis=l,this.frame=r,t+=1}if(!this.parent)throw new Error("Cannot repaint itemset: no parent attached");var p=this.parent.getContainer();if(!p)throw new Error("Cannot repaint itemset: parent has no container element"); -r.parentNode||(p.appendChild(r),t+=1),this.dom.axis.parentNode||(p.appendChild(this.dom.axis),t+=1),t+=e(r.style,"left",i(n.left,"0px")),t+=e(r.style,"top",i(n.top,"0px")),t+=e(r.style,"width",i(n.width,"100%")),t+=e(r.style,"height",i(n.height,this.height+"px")),t+=e(this.dom.axis.style,"left",i(n.left,"0px")),t+=e(this.dom.axis.style,"width",i(n.width,"100%")),t+="bottom"==s?e(this.dom.axis.style,"top",this.height+this.top+"px"):e(this.dom.axis.style,"top",this.top+"px"),this._updateConversion();var u=this,c=this.queue,f=this.itemsData,m=this.items,v={};return Object.keys(c).forEach(function(e){var i=c[e],s=m[e];switch(i){case"add":case"update":var r=f&&f.get(e,v);if(r){var a=r.type||r.start&&r.end&&"range"||n.type||"box",h=g.types[a];if(s&&(h&&s instanceof h?(s.data=r,t++):(t+=s.hide(),s=null)),!s){if(!h)throw new TypeError('Unknown item type "'+a+'"');s=new h(u,r,n,o),t++}s.repaint(),m[e]=s}delete c[e];break;case"remove":s&&(t+=s.hide()),delete m[e],delete c[e];break;default:console.log('Error: unknown action "'+i+'"')}}),k.forEach(this.items,function(e){e.visible?(t+=e.show(),e.reposition()):t+=e.hide()}),t>0},g.prototype.getForeground=function(){return this.dom.foreground},g.prototype.getBackground=function(){return this.dom.background},g.prototype.getAxis=function(){return this.dom.axis},g.prototype.reflow=function(){var t=0,e=this.options,i=e.margin&&e.margin.axis||this.defaultOptions.margin.axis,n=e.margin&&e.margin.item||this.defaultOptions.margin.item,s=k.updateProperty,o=k.option.asNumber,r=k.option.asSize,a=this.frame;if(a){this._updateConversion(),k.forEach(this.items,function(e){t+=e.reflow()}),this.stack.update();var h,d=o(e.maxHeight),l=null!=r(e.height);if(l)h=a.offsetHeight;else{var p=this.stack.ordered;if(p.length){var u=p[0].top,c=p[0].top+p[0].height;k.forEach(p,function(t){u=Math.min(u,t.top),c=Math.max(c,t.top+t.height)}),h=c-u+i+n}else h=i+n}null!=d&&(h=Math.min(h,d)),t+=s(this,"height",h),t+=s(this,"top",a.offsetTop),t+=s(this,"left",a.offsetLeft),t+=s(this,"width",a.offsetWidth)}else t+=1;return t>0},g.prototype.hide=function(){var t=!1;return this.frame&&this.frame.parentNode&&(this.frame.parentNode.removeChild(this.frame),t=!0),this.dom.axis&&this.dom.axis.parentNode&&(this.dom.axis.parentNode.removeChild(this.dom.axis),t=!0),t},g.prototype.setItems=function(t){var e,i=this,n=this.itemsData;if(t){if(!(t instanceof o||t instanceof r))throw new TypeError("Data must be an instance of DataSet");this.itemsData=t}else this.itemsData=null;if(n&&(k.forEach(this.listeners,function(t,e){n.unsubscribe(e,t)}),e=n.getIds(),this._onRemove(e)),this.itemsData){var s=this.id;k.forEach(this.listeners,function(t,e){i.itemsData.subscribe(e,t,s)}),e=this.itemsData.getIds(),this._onAdd(e)}},g.prototype.getItems=function(){return this.itemsData},g.prototype._onUpdate=function(t){this._toQueue("update",t)},g.prototype._onAdd=function(t){this._toQueue("add",t)},g.prototype._onRemove=function(t){this._toQueue("remove",t)},g.prototype._toQueue=function(t,e){var i=this.queue;e.forEach(function(e){i[e]=t}),this.controller&&this.requestRepaint()},g.prototype._updateConversion=function(){var t=this.range;if(!t)throw new Error("No range configured");this.conversion=t.conversion?t.conversion(this.width):h.conversion(t.start,t.end,this.width)},g.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.factor+e.offset)},g.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.factor},v.prototype.select=function(){this.selected=!0},v.prototype.unselect=function(){this.selected=!1},v.prototype.show=function(){return!1},v.prototype.hide=function(){return!1},v.prototype.repaint=function(){return!1},v.prototype.reflow=function(){return!1},v.prototype.getWidth=function(){return this.width},y.prototype=new v(null,null),y.prototype.select=function(){this.selected=!0},y.prototype.unselect=function(){this.selected=!1},y.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw new Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw new Error("Cannot repaint time axis: parent has no foreground container element");var n=this.parent.getBackground();if(!n)throw new Error("Cannot repaint time axis: parent has no background container element");var s=this.parent.getAxis();if(!n)throw new Error("Cannot repaint time axis: parent has no axis container element");if(e.box.parentNode||(i.appendChild(e.box),t=!0),e.line.parentNode||(n.appendChild(e.line),t=!0),e.dot.parentNode||(s.appendChild(e.dot),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw new Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var o=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=o&&(this.className=o,e.box.className="item box"+o,e.line.className="item line"+o,e.dot.className="item dot"+o,t=!0)}return t},y.prototype.show=function(){return this.dom&&this.dom.box.parentNode?!1:this.repaint()},y.prototype.hide=function(){var t=!1,e=this.dom;return e&&(e.box.parentNode&&(e.box.parentNode.removeChild(e.box),t=!0),e.line.parentNode&&e.line.parentNode.removeChild(e.line),e.dot.parentNode&&e.dot.parentNode.removeChild(e.dot)),t},y.prototype.reflow=function(){var t,e,i,n,s,o,r,a,h,d,l,p,u=0;if(void 0==this.data.start)throw new Error('Property "start" missing in item '+this.data.id);if(l=this.data,p=this.parent&&this.parent.range,l&&p){var c=p.end-p.start;this.visible=l.start>p.start-c&&l.start0},y.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.box=document.createElement("DIV"),t.content=document.createElement("DIV"),t.content.className="content",t.box.appendChild(t.content),t.line=document.createElement("DIV"),t.line.className="line",t.dot=document.createElement("DIV"),t.dot.className="dot")},y.prototype.reposition=function(){var t=this.dom,e=this.props,i=this.options.orientation||this.defaultOptions.orientation;if(t){var n=t.box,s=t.line,o=t.dot;n.style.left=this.left+"px",n.style.top=this.top+"px",s.style.left=e.line.left+"px","top"==i?(s.style.top="0px",s.style.height=this.top+"px"):(s.style.top=this.top+this.height+"px",s.style.height=Math.max(this.parent.height-this.top-this.height+this.props.dot.height/2,0)+"px"),o.style.left=e.dot.left+"px",o.style.top=e.dot.top+"px"}},w.prototype=new v(null,null),w.prototype.select=function(){this.selected=!0},w.prototype.unselect=function(){this.selected=!1},w.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw new Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw new Error("Cannot repaint time axis: parent has no foreground container element");if(e.point.parentNode||(i.appendChild(e.point),i.appendChild(e.point),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw new Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var n=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=n&&(this.className=n,e.point.className="item point"+n,t=!0)}return t},w.prototype.show=function(){return this.dom&&this.dom.point.parentNode?!1:this.repaint()},w.prototype.hide=function(){var t=!1,e=this.dom;return e&&e.point.parentNode&&(e.point.parentNode.removeChild(e.point),t=!0),t},w.prototype.reflow=function(){var t,e,i,n,s,o,r,a,h,d,l=0;if(void 0==this.data.start)throw new Error('Property "start" missing in item '+this.data.id);if(h=this.data,d=this.parent&&this.parent.range,h&&d){var p=d.end-d.start;this.visible=h.start>d.start-p&&h.start0},w.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.point=document.createElement("div"),t.content=document.createElement("div"),t.content.className="content",t.point.appendChild(t.content),t.dot=document.createElement("div"),t.dot.className="dot",t.point.appendChild(t.dot))},w.prototype.reposition=function(){var t=this.dom,e=this.props;t&&(t.point.style.top=this.top+"px",t.point.style.left=this.left+"px",t.content.style.marginLeft=e.content.marginLeft+"px",t.dot.style.top=e.dot.top+"px")},_.prototype=new v(null,null),_.prototype.select=function(){this.selected=!0},_.prototype.unselect=function(){this.selected=!1},_.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw new Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw new Error("Cannot repaint time axis: parent has no foreground container element");if(e.box.parentNode||(i.appendChild(e.box),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw new Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var n=this.data.className?" "+this.data.className:"";this.className!=n&&(this.className=n,e.box.className="item range"+n,t=!0)}return t},_.prototype.show=function(){return this.dom&&this.dom.box.parentNode?!1:this.repaint()},_.prototype.hide=function(){var t=!1,e=this.dom;return e&&e.box.parentNode&&(e.box.parentNode.removeChild(e.box),t=!0),t},_.prototype.reflow=function(){var t,e,i,n,s,o,r,a,h,d,l,p,u,c,f,m,g=0;if(void 0==this.data.start)throw new Error('Property "start" missing in item '+this.data.id);if(void 0==this.data.end)throw new Error('Property "end" missing in item '+this.data.id);return h=this.data,d=this.parent&&this.parent.range,this.visible=h&&d?h.startd.start:!1,this.visible&&(t=this.dom,t?(e=this.props,i=this.options,o=this.parent,r=o.toScreen(this.data.start),a=o.toScreen(this.data.end),l=k.updateProperty,p=t.box,u=o.width,f=i.orientation||this.defaultOptions.orientation,n=i.margin&&i.margin.axis||this.defaultOptions.margin.axis,s=i.padding||this.defaultOptions.padding,g+=l(e.content,"width",t.content.offsetWidth),g+=l(this,"height",p.offsetHeight),-u>r&&(r=-u),a>2*u&&(a=2*u),c=0>r?Math.min(-r,a-r-e.content.width-2*s):0,g+=l(e.content,"left",c),"top"==f?(m=n,g+=l(this,"top",m)):(m=o.height-this.height-n,g+=l(this,"top",m)),g+=l(this,"left",r),g+=l(this,"width",Math.max(a-r,1))):g+=1),g>0},_.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.box=document.createElement("div"),t.content=document.createElement("div"),t.content.className="content",t.box.appendChild(t.content))},_.prototype.reposition=function(){var t=this.dom,e=this.props;t&&(t.box.style.top=this.top+"px",t.box.style.left=this.left+"px",t.box.style.width=this.width+"px",t.content.style.left=e.content.left+"px")},b.prototype=new _(null,null),b.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw new Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw new Error("Cannot repaint time axis: parent has no foreground container element");if(e.box.parentNode||(i.appendChild(e.box),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw new Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var n=this.data.className?" "+this.data.className:"";this.className!=n&&(this.className=n,e.box.className="item rangeoverflow"+n,t=!0)}return t},b.prototype.getWidth=function(){return void 0!==this.props.content&&this.width0},T.prototype=new p,T.prototype.setOptions=l.prototype.setOptions,T.prototype.setRange=function(){},T.prototype.setItems=function(t){this.itemsData=t;for(var e in this.groups)if(this.groups.hasOwnProperty(e)){var i=this.groups[e];i.setItems(t)}},T.prototype.getItems=function(){return this.itemsData},T.prototype.setRange=function(t){this.range=t},T.prototype.setGroups=function(t){var e,i=this;if(this.groupsData&&(k.forEach(this.listeners,function(t,e){i.groupsData.unsubscribe(e,t)}),e=this.groupsData.getIds(),this._onRemove(e)),t?t instanceof o?this.groupsData=t:(this.groupsData=new o({convert:{start:"Date",end:"Date"}}),this.groupsData.add(t)):this.groupsData=null,this.groupsData){var n=this.id;k.forEach(this.listeners,function(t,e){i.groupsData.subscribe(e,t,n)}),e=this.groupsData.getIds(),this._onAdd(e)}},T.prototype.getGroups=function(){return this.groupsData},T.prototype.repaint=function(){var t,e,i,n,s=0,o=k.updateProperty,r=k.option.asSize,a=k.option.asElement,h=this.options,d=this.dom.frame,l=this.dom.labels;if(!this.parent)throw new Error("Cannot repaint groupset: no parent attached");var p=this.parent.getContainer();if(!p)throw new Error("Cannot repaint groupset: parent has no container element");if(!d){d=document.createElement("div"),d.className="groupset",this.dom.frame=d;var u=h.className;u&&k.addClassName(d,k.option.asString(u)),s+=1}d.parentNode||(p.appendChild(d),s+=1);var c=a(h.labelContainer);if(!c)throw new Error('Cannot repaint groupset: option "labelContainer" not defined');l||(l=document.createElement("div"),l.className="labels",this.dom.labels=l),l.parentNode&&l.parentNode==c||(l.parentNode&&l.parentNode.removeChild(l.parentNode),c.appendChild(l)),s+=o(d.style,"height",r(h.height,this.height+"px")),s+=o(d.style,"top",r(h.top,"0px")),s+=o(d.style,"left",r(h.left,"0px")),s+=o(d.style,"width",r(h.width,"100%")),s+=o(l.style,"top",r(h.top,"0px"));var f=this,m=this.queue,g=this.groups,v=this.groupsData,y=Object.keys(m);if(y.length){y.forEach(function(t){var e=m[t],i=g[t];switch(e){case"add":case"update":if(!i){var n=Object.create(f.options);i=new E(f,t,n),i.setItems(f.itemsData),g[t]=i,f.controller.add(i)}i.data=v.get(t),delete m[t];break;case"remove":i&&(i.setItems(),delete g[t],f.controller.remove(i)),delete m[t];break;default:console.log('Error: unknown action "'+e+'"')}});var w=this.groupsData.getIds({order:this.options.groupsOrder});for(t=0;t0},T.prototype._createLabel=function(t){var e=this.groups[t],i=document.createElement("div");i.className="label";var n=document.createElement("div");n.className="inner",i.appendChild(n);var s=e.data&&e.data.content;s instanceof Element?n.appendChild(s):void 0!=s&&(n.innerHTML=s);var o=e.data&&e.data.className;return o&&k.addClassName(i,o),e.label=i,i},T.prototype.getContainer=function(){return this.dom.frame},T.prototype.getLabelsWidth=function(){return this.props.labels.width},T.prototype.reflow=function(){var t,e,i=0,n=this.options,s=k.updateProperty,o=k.option.asNumber,r=k.option.asSize,a=this.dom.frame;if(a){var h,d=o(n.maxHeight),l=null!=r(n.height);if(l)h=a.offsetHeight;else{h=0;for(t in this.groups)this.groups.hasOwnProperty(t)&&(e=this.groups[t],h+=e.height)}null!=d&&(h=Math.min(h,d)),i+=s(this,"height",h),i+=s(this,"top",a.offsetTop),i+=s(this,"left",a.offsetLeft),i+=s(this,"width",a.offsetWidth)}var p=0;for(t in this.groups)if(this.groups.hasOwnProperty(t)){e=this.groups[t];var u=e.props&&e.props.label&&e.props.label.width||0;p=Math.max(p,u)}return i+=s(this.props.labels,"width",p),i>0},T.prototype.hide=function(){return this.dom.frame&&this.dom.frame.parentNode?(this.dom.frame.parentNode.removeChild(this.dom.frame),!0):!1},T.prototype.show=function(){return this.dom.frame&&this.dom.frame.parentNode?!1:this.repaint()},T.prototype._onUpdate=function(t){this._toQueue(t,"update")},T.prototype._onAdd=function(t){this._toQueue(t,"add")},T.prototype._onRemove=function(t){this._toQueue(t,"remove")},T.prototype._toQueue=function(t,e){var i=this.queue;t.forEach(function(t){i[t]=e}),this.controller&&this.requestRepaint()},x.prototype.setOptions=function(t){k.extend(this.options,t),this.range.setRange(),this.controller.reflow(),this.controller.repaint()},x.prototype.setCustomTime=function(t){this.customtime._setCustomTime(t)},x.prototype.getCustomTime=function(){return new Date(this.customtime.customTime.valueOf())},x.prototype.setItems=function(t){var e,i=null==this.itemsData;if(t?t instanceof o&&(e=t):e=null,t instanceof o||(e=new o({convert:{start:"Date",end:"Date"}}),e.add(t)),this.itemsData=e,this.content.setItems(e),i&&(void 0==this.options.start||void 0==this.options.end)){var n=this.getItemRange(),s=n.min,r=n.max;if(null!=s&&null!=r){var a=r.valueOf()-s.valueOf();0>=a&&(a=864e5),s=new Date(s.valueOf()-.05*a),r=new Date(r.valueOf()+.05*a)}void 0!=this.options.start&&(s=k.convert(this.options.start,"Date")),void 0!=this.options.end&&(r=k.convert(this.options.end,"Date")),(null!=s||null!=r)&&this.range.setRange(s,r)}},x.prototype.setGroups=function(t){var e=this;this.groupsData=t;var i=this.groupsData?T:g;if(!(this.content instanceof i)){this.content&&(this.content.hide(),this.content.setItems&&this.content.setItems(),this.content.setGroups&&this.content.setGroups(),this.controller.remove(this.content));var n=Object.create(this.options);k.extend(n,{top:function(){return"top"==e.options.orientation?e.timeaxis.height:e.itemPanel.height-e.timeaxis.height-e.content.height},left:null,width:"100%",height:function(){return e.options.height?e.itemPanel.height-e.timeaxis.height:null},maxHeight:function(){if(e.options.maxHeight){if(!k.isNumber(e.options.maxHeight))throw new TypeError("Number expected for property maxHeight");return e.options.maxHeight-e.timeaxis.height}return null},labelContainer:function(){return e.labelPanel.getContainer()}}),this.content=new i(this.itemPanel,[this.timeaxis],n),this.content.setRange&&this.content.setRange(this.range),this.content.setItems&&this.content.setItems(this.itemsData),this.content.setGroups&&this.content.setGroups(this.groupsData),this.controller.add(this.content)}},x.prototype.getItemRange=function(){var t=this.itemsData,e=null,i=null;if(t){var n=t.min("start");e=n?n.start.valueOf():null;var s=t.max("start");s&&(i=s.start.valueOf());var o=t.max("end");o&&(i=null==i?o.end.valueOf():Math.max(i,o.end.valueOf()))}return{min:null!=e?new Date(e):null,max:null!=i?new Date(i):null}},function(t){function e(t){return M=t,u()}function i(){D=0,C=M.charAt(0)}function n(){D++,C=M.charAt(D)}function s(){return M.charAt(D+1)}function o(t){return L.test(t)}function r(t,e){if(t||(t={}),e)for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return t}function a(t,e,i){for(var n=e.split("."),s=t;n.length;){var o=n.shift();n.length?(s[o]||(s[o]={}),s=s[o]):s[o]=i}}function h(t,e){for(var i,n,s=null,o=[t],a=t;a.parent;)o.push(a.parent),a=a.parent;if(a.nodes)for(i=0,n=a.nodes.length;n>i;i++)if(e.id===a.nodes[i].id){s=a.nodes[i];break}for(s||(s={id:e.id},t.node&&(s.attr=r(s.attr,t.node))),i=o.length-1;i>=0;i--){var h=o[i];h.nodes||(h.nodes=[]),-1==h.nodes.indexOf(s)&&h.nodes.push(s)}e.attr&&(s.attr=r(s.attr,e.attr))}function d(t,e){if(t.edges||(t.edges=[]),t.edges.push(e),t.edge){var i=r({},t.edge);e.attr=r(i,e.attr)}}function l(t,e,i,n,s){var o={from:e,to:i,type:n};return t.edge&&(o.attr=r({},t.edge)),o.attr=r(o.attr||{},s),o}function p(){for(N=x.NULL,O="";" "==C||" "==C||"\n"==C||"\r"==C;)n();do{var t=!1;if("#"==C){for(var e=D-1;" "==M.charAt(e)||" "==M.charAt(e);)e--;if("\n"==M.charAt(e)||""==M.charAt(e)){for(;""!=C&&"\n"!=C;)n();t=!0}}if("/"==C&&"/"==s()){for(;""!=C&&"\n"!=C;)n();t=!0}if("/"==C&&"*"==s()){for(;""!=C;){if("*"==C&&"/"==s()){n(),n();break}n()}t=!0}for(;" "==C||" "==C||"\n"==C||"\r"==C;)n()}while(t);if(""==C)return N=x.DELIMITER,void 0;var i=C+s();if(S[i])return N=x.DELIMITER,O=i,n(),n(),void 0;if(S[C])return N=x.DELIMITER,O=C,n(),void 0;if(o(C)||"-"==C){for(O+=C,n();o(C);)O+=C,n();return"false"==O?O=!1:"true"==O?O=!0:isNaN(Number(O))||(O=Number(O)),N=x.IDENTIFIER,void 0}if('"'==C){for(n();""!=C&&('"'!=C||'"'==C&&'"'==s());)O+=C,'"'==C&&n(),n();if('"'!=C)throw _('End of string " expected');return n(),N=x.IDENTIFIER,void 0}for(N=x.UNKNOWN;""!=C;)O+=C,n();throw new SyntaxError('Syntax error in part "'+b(O,30)+'"')}function u(){var t={};if(i(),p(),"strict"==O&&(t.strict=!0,p()),("graph"==O||"digraph"==O)&&(t.type=O,p()),N==x.IDENTIFIER&&(t.id=O,p()),"{"!=O)throw _("Angle bracket { expected");if(p(),c(t),"}"!=O)throw _("Angle bracket } expected");if(p(),""!==O)throw _("End of file expected");return p(),delete t.node,delete t.edge,delete t.graph,t}function c(t){for(;""!==O&&"}"!=O;)f(t),";"==O&&p()}function f(t){var e=m(t);if(e)return y(t,e),void 0;var i=g(t);if(!i){if(N!=x.IDENTIFIER)throw _("Identifier expected");var n=O;if(p(),"="==O){if(p(),N!=x.IDENTIFIER)throw _("Identifier expected");t[n]=O,p()}else v(t,n)}}function m(t){var e=null;if("subgraph"==O&&(e={},e.type="subgraph",p(),N==x.IDENTIFIER&&(e.id=O,p())),"{"==O){if(p(),e||(e={}),e.parent=t,e.node=t.node,e.edge=t.edge,e.graph=t.graph,c(e),"}"!=O)throw _("Angle bracket } expected");p(),delete e.node,delete e.edge,delete e.graph,delete e.parent,t.subgraphs||(t.subgraphs=[]),t.subgraphs.push(e)}return e}function g(t){return"node"==O?(p(),t.node=w(),"node"):"edge"==O?(p(),t.edge=w(),"edge"):"graph"==O?(p(),t.graph=w(),"graph"):null}function v(t,e){var i={id:e},n=w();n&&(i.attr=n),h(t,i),y(t,e)}function y(t,e){for(;"->"==O||"--"==O;){var i,n=O;p();var s=m(t);if(s)i=s;else{if(N!=x.IDENTIFIER)throw _("Identifier or subgraph expected");i=O,h(t,{id:i}),p()}var o=w(),r=l(t,e,i,n,o);d(t,r),e=i}}function w(){for(var t=null;"["==O;){for(p(),t={};""!==O&&"]"!=O;){if(N!=x.IDENTIFIER)throw _("Attribute name expected");var e=O;if(p(),"="!=O)throw _("Equal sign = expected");if(p(),N!=x.IDENTIFIER)throw _("Attribute value expected");var i=O;a(t,e,i),p(),","==O&&p()}if("]"!=O)throw _("Bracket ] expected");p()}return t}function _(t){return new SyntaxError(t+', got "'+b(O,30)+'" (char '+D+")")}function b(t,e){return t.length<=e?t:t.substr(0,27)+"..."}function E(t,e,i){t instanceof Array?t.forEach(function(t){e instanceof Array?e.forEach(function(e){i(t,e)}):i(t,e)}):e instanceof Array?e.forEach(function(e){i(t,e)}):i(t,e)}function T(t){function i(t){var e={from:t.from,to:t.to};return r(e,t.attr),e.style="->"==t.type?"arrow":"line",e}var n=e(t),s={nodes:[],edges:[],options:{}};return n.nodes&&n.nodes.forEach(function(t){var e={id:t.id,label:String(t.label||t.id)};r(e,t.attr),e.image&&(e.shape="image"),s.nodes.push(e)}),n.edges&&n.edges.forEach(function(t){var e,n;e=t.from instanceof Object?t.from.nodes:{id:t.from},n=t.to instanceof Object?t.to.nodes:{id:t.to},t.from instanceof Object&&t.from.edges&&t.from.edges.forEach(function(t){var e=i(t);s.edges.push(e)}),E(e,n,function(e,n){var o=l(s,e.id,n.id,t.type,t.attr),r=i(o);s.edges.push(r)}),t.to instanceof Object&&t.to.edges&&t.to.edges.forEach(function(t){var e=i(t);s.edges.push(e)})}),n.attr&&(s.options=n.attr),s}var x={NULL:0,DELIMITER:1,IDENTIFIER:2,UNKNOWN:3},S={"{":!0,"}":!0,"[":!0,"]":!0,";":!0,"=":!0,",":!0,"->":!0,"--":!0},M="",D=0,C="",O="",N=x.NULL,L=/[a-zA-Z_0-9.:#]/;t.parseDOT=e,t.DOTToGraph=T}("undefined"!=typeof k?k:n),"undefined"!=typeof CanvasRenderingContext2D&&(CanvasRenderingContext2D.prototype.circle=function(t,e,i){this.beginPath(),this.arc(t,e,i,0,2*Math.PI,!1)},CanvasRenderingContext2D.prototype.square=function(t,e,i){this.beginPath(),this.rect(t-i,e-i,2*i,2*i)},CanvasRenderingContext2D.prototype.triangle=function(t,e,i){this.beginPath();var n=2*i,s=n/2,o=Math.sqrt(3)/6*n,r=Math.sqrt(n*n-s*s);this.moveTo(t,e-(r-o)),this.lineTo(t+s,e+o),this.lineTo(t-s,e+o),this.lineTo(t,e-(r-o)),this.closePath()},CanvasRenderingContext2D.prototype.triangleDown=function(t,e,i){this.beginPath();var n=2*i,s=n/2,o=Math.sqrt(3)/6*n,r=Math.sqrt(n*n-s*s);this.moveTo(t,e+(r-o)),this.lineTo(t+s,e-o),this.lineTo(t-s,e-o),this.lineTo(t,e+(r-o)),this.closePath()},CanvasRenderingContext2D.prototype.star=function(t,e,i){this.beginPath();for(var n=0;10>n;n++){var s=0===n%2?1.3*i:.5*i;this.lineTo(t+s*Math.sin(2*n*Math.PI/10),e-s*Math.cos(2*n*Math.PI/10))}this.closePath()},CanvasRenderingContext2D.prototype.roundRect=function(t,e,i,n,s){var o=Math.PI/180;0>i-2*s&&(s=i/2),0>n-2*s&&(s=n/2),this.beginPath(),this.moveTo(t+s,e),this.lineTo(t+i-s,e),this.arc(t+i-s,e+s,s,270*o,360*o,!1),this.lineTo(t+i,e+n-s),this.arc(t+i-s,e+n-s,s,0,90*o,!1),this.lineTo(t+s,e+n),this.arc(t+s,e+n-s,s,90*o,180*o,!1),this.lineTo(t,e+s),this.arc(t+s,e+s,s,180*o,270*o,!1)},CanvasRenderingContext2D.prototype.ellipse=function(t,e,i,n){var s=.5522848,o=i/2*s,r=n/2*s,a=t+i,h=e+n,d=t+i/2,l=e+n/2;this.beginPath(),this.moveTo(t,l),this.bezierCurveTo(t,l-r,d-o,e,d,e),this.bezierCurveTo(d+o,e,a,l-r,a,l),this.bezierCurveTo(a,l+r,d+o,h,d,h),this.bezierCurveTo(d-o,h,t,l+r,t,l)},CanvasRenderingContext2D.prototype.database=function(t,e,i,n){var s=1/3,o=i,r=n*s,a=.5522848,h=o/2*a,d=r/2*a,l=t+o,p=e+r,u=t+o/2,c=e+r/2,f=e+(n-r/2),m=e+n;this.beginPath(),this.moveTo(l,c),this.bezierCurveTo(l,c+d,u+h,p,u,p),this.bezierCurveTo(u-h,p,t,c+d,t,c),this.bezierCurveTo(t,c-d,u-h,e,u,e),this.bezierCurveTo(u+h,e,l,c-d,l,c),this.lineTo(l,f),this.bezierCurveTo(l,f+d,u+h,m,u,m),this.bezierCurveTo(u-h,m,t,f+d,t,f),this.lineTo(t,c)},CanvasRenderingContext2D.prototype.arrow=function(t,e,i,n){var s=t-n*Math.cos(i),o=e-n*Math.sin(i),r=t-.9*n*Math.cos(i),a=e-.9*n*Math.sin(i),h=s+n/3*Math.cos(i+.5*Math.PI),d=o+n/3*Math.sin(i+.5*Math.PI),l=s+n/3*Math.cos(i-.5*Math.PI),p=o+n/3*Math.sin(i-.5*Math.PI);this.beginPath(),this.moveTo(t,e),this.lineTo(h,d),this.lineTo(r,a),this.lineTo(l,p),this.closePath()},CanvasRenderingContext2D.prototype.dashedLine=function(t,e,i,n,s){s||(s=[10,5]),0==u&&(u=.001);var o=s.length;this.moveTo(t,e);for(var r=i-t,a=n-e,h=a/r,d=Math.sqrt(r*r+a*a),l=0,p=!0;d>=.1;){var u=s[l++%o];u>d&&(u=d);var c=Math.sqrt(u*u/(1+h*h));0>r&&(c=-c),t+=c,e+=h*c,this[p?"lineTo":"moveTo"](t,e),d-=u,p=!p}}),S.prototype.attachEdge=function(t){-1==this.edges.indexOf(t)&&this.edges.push(t),this._updateMass()},S.prototype.detachEdge=function(t){var e=this.edges.indexOf(t);-1!=e&&this.edges.splice(e,1),this._updateMass()},S.prototype._updateMass=function(){this.mass=50+20*this.edges.length},S.prototype.setProperties=function(t,e){if(t){if(void 0!=t.id&&(this.id=t.id),void 0!=t.label&&(this.label=t.label),void 0!=t.title&&(this.title=t.title),void 0!=t.group&&(this.group=t.group),void 0!=t.x&&(this.x=t.x),void 0!=t.y&&(this.y=t.y),void 0!=t.value&&(this.value=t.value),void 0===this.id)throw"Node must have an id";if(this.group){var i=this.grouplist.get(this.group);for(var n in i)i.hasOwnProperty(n)&&(this[n]=i[n])}if(void 0!=t.shape&&(this.shape=t.shape),void 0!=t.image&&(this.image=t.image),void 0!=t.radius&&(this.radius=t.radius),void 0!=t.color&&(this.color=S.parseColor(t.color)),void 0!=t.fontColor&&(this.fontColor=t.fontColor),void 0!=t.fontSize&&(this.fontSize=t.fontSize),void 0!=t.fontFace&&(this.fontFace=t.fontFace),void 0!=this.image){if(!this.imagelist)throw"No imagelist provided";this.imageObj=this.imagelist.load(this.image)}switch(this.xFixed=this.xFixed||void 0!=t.x,this.yFixed=this.yFixed||void 0!=t.y,this.radiusFixed=this.radiusFixed||void 0!=t.radius,"image"==this.shape&&(this.radiusMin=e.nodes.widthMin,this.radiusMax=e.nodes.widthMax),this.shape){case"database":this.draw=this._drawDatabase,this.resize=this._resizeDatabase;break;case"box":this.draw=this._drawBox,this.resize=this._resizeBox;break;case"circle":this.draw=this._drawCircle,this.resize=this._resizeCircle;break;case"ellipse":this.draw=this._drawEllipse,this.resize=this._resizeEllipse;break;case"image":this.draw=this._drawImage,this.resize=this._resizeImage;break;case"text":this.draw=this._drawText,this.resize=this._resizeText;break;case"dot":this.draw=this._drawDot,this.resize=this._resizeShape;break;case"square":this.draw=this._drawSquare,this.resize=this._resizeShape;break;case"triangle":this.draw=this._drawTriangle,this.resize=this._resizeShape;break;case"triangleDown":this.draw=this._drawTriangleDown,this.resize=this._resizeShape;break;case"star":this.draw=this._drawStar,this.resize=this._resizeShape;break;default:this.draw=this._drawEllipse,this.resize=this._resizeEllipse}this._reset()}},S.parseColor=function(t){var e;return k.isString(t)?e={border:t,background:t,highlight:{border:t,background:t}}:(e={},e.background=t.background||"white",e.border=t.border||e.background,k.isString(t.highlight)?e.highlight={border:t.highlight,background:t.highlight}:(e.highlight={},e.highlight.background=t.highlight&&t.highlight.background||e.background,e.highlight.border=t.highlight&&t.highlight.border||e.border)),e},S.prototype.select=function(){this.selected=!0,this._reset()},S.prototype.unselect=function(){this.selected=!1,this._reset()},S.prototype._reset=function(){this.width=void 0,this.height=void 0},S.prototype.getTitle=function(){return this.title},S.prototype.distanceToBorder=function(t,e){var i=1;switch(this.width||this.resize(t),this.shape){case"circle":case"dot":return this.radius+i;case"ellipse":var n=this.width/2,s=this.height/2,o=Math.sin(e)*n,r=Math.cos(e)*s; -return n*s/Math.sqrt(o*o+r*r);case"box":case"image":case"text":default:return this.width?Math.min(Math.abs(this.width/2/Math.cos(e)),Math.abs(this.height/2/Math.sin(e)))+i:0}},S.prototype._setForce=function(t,e){this.fx=t,this.fy=e},S.prototype._addForce=function(t,e){this.fx+=t,this.fy+=e},S.prototype.discreteStep=function(t){if(!this.xFixed){var e=-this.damping*this.vx,i=(this.fx+e)/this.mass;this.vx+=i/t,this.x+=this.vx/t}if(!this.yFixed){var n=-this.damping*this.vy,s=(this.fy+n)/this.mass;this.vy+=s/t,this.y+=this.vy/t}},S.prototype.isFixed=function(){return this.xFixed&&this.yFixed},S.prototype.isMoving=function(t){return Math.abs(this.vx)>t||Math.abs(this.vy)>t||!this.xFixed&&Math.abs(this.fx)>this.minForce||!this.yFixed&&Math.abs(this.fy)>this.minForce},S.prototype.isSelected=function(){return this.selected},S.prototype.getValue=function(){return this.value},S.prototype.getDistance=function(t,e){var i=this.x-t,n=this.y-e;return Math.sqrt(i*i+n*n)},S.prototype.setValueRange=function(t,e){if(!this.radiusFixed&&void 0!==this.value)if(e==t)this.radius=(this.radiusMin+this.radiusMax)/2;else{var i=(this.radiusMax-this.radiusMin)/(e-t);this.radius=(this.value-t)*i+this.radiusMin}},S.prototype.draw=function(){throw"Draw method not initialized for node"},S.prototype.resize=function(){throw"Resize method not initialized for node"},S.prototype.isOverlappingWith=function(t){return this.leftt.left&&this.topt.top},S.prototype._resizeImage=function(){if(!this.width){var t,e;if(this.value){var i=this.imageObj.height/this.imageObj.width;t=this.radius||this.imageObj.width,e=this.radius*i||this.imageObj.height}else t=this.imageObj.width,e=this.imageObj.height;this.width=t,this.height=e}},S.prototype._drawImage=function(t){this._resizeImage(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e;this.imageObj?(t.drawImage(this.imageObj,this.left,this.top,this.width,this.height),e=this.y+this.height/2):e=this.y,this._label(t,this.label,this.x,e,void 0,"top")},S.prototype._resizeBox=function(t){if(!this.width){var e=5,i=this.getTextSize(t);this.width=i.width+2*e,this.height=i.height+2*e}},S.prototype._drawBox=function(t){this._resizeBox(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.lineWidth=this.selected?2:1,t.roundRect(this.left,this.top,this.width,this.height,this.radius),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},S.prototype._resizeDatabase=function(t){if(!this.width){var e=5,i=this.getTextSize(t),n=i.width+2*e;this.width=n,this.height=n}},S.prototype._drawDatabase=function(t){this._resizeDatabase(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.lineWidth=this.selected?2:1,t.database(this.x-this.width/2,this.y-.5*this.height,this.width,this.height),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},S.prototype._resizeCircle=function(t){if(!this.width){var e=5,i=this.getTextSize(t),n=Math.max(i.width,i.height)+2*e;this.radius=n/2,this.width=n,this.height=n}},S.prototype._drawCircle=function(t){this._resizeCircle(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.lineWidth=this.selected?2:1,t.circle(this.x,this.y,this.radius),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},S.prototype._resizeEllipse=function(t){if(!this.width){var e=this.getTextSize(t);this.width=1.5*e.width,this.height=2*e.height,this.widthl;l++)t.fillText(r[l],i,d),d+=h}},S.prototype.getTextSize=function(t){if(void 0!=this.label){t.font=(this.selected?"bold ":"")+this.fontSize+"px "+this.fontFace;for(var e=this.label.split("\n"),i=(this.fontSize+4)*e.length,n=0,s=0,o=e.length;o>s;s++)n=Math.max(n,t.measureText(e[s]).width);return{width:n,height:i}}return{width:0,height:0}},M.prototype.setProperties=function(t,e){if(t)switch(void 0!=t.from&&(this.fromId=t.from),void 0!=t.to&&(this.toId=t.to),void 0!=t.id&&(this.id=t.id),void 0!=t.style&&(this.style=t.style),void 0!=t.label&&(this.label=t.label),this.label&&(this.fontSize=e.edges.fontSize,this.fontFace=e.edges.fontFace,this.fontColor=e.edges.fontColor,void 0!=t.fontColor&&(this.fontColor=t.fontColor),void 0!=t.fontSize&&(this.fontSize=t.fontSize),void 0!=t.fontFace&&(this.fontFace=t.fontFace)),void 0!=t.title&&(this.title=t.title),void 0!=t.width&&(this.width=t.width),void 0!=t.value&&(this.value=t.value),void 0!=t.length&&(this.length=t.length),t.dash&&(void 0!=t.dash.length&&(this.dash.length=t.dash.length),void 0!=t.dash.gap&&(this.dash.gap=t.dash.gap),void 0!=t.dash.altLength&&(this.dash.altLength=t.dash.altLength)),void 0!=t.color&&(this.color=t.color),this.connect(),this.widthFixed=this.widthFixed||void 0!=t.width,this.lengthFixed=this.lengthFixed||void 0!=t.length,this.stiffness=1/this.length,this.style){case"line":this.draw=this._drawLine;break;case"arrow":this.draw=this._drawArrow;break;case"arrow-center":this.draw=this._drawArrowCenter;break;case"dash-line":this.draw=this._drawDashLine;break;default:this.draw=this._drawLine}},M.prototype.connect=function(){this.disconnect(),this.from=this.graph.nodes[this.fromId]||null,this.to=this.graph.nodes[this.toId]||null,this.connected=this.from&&this.to,this.connected?(this.from.attachEdge(this),this.to.attachEdge(this)):(this.from&&this.from.detachEdge(this),this.to&&this.to.detachEdge(this))},M.prototype.disconnect=function(){this.from&&(this.from.detachEdge(this),this.from=null),this.to&&(this.to.detachEdge(this),this.to=null),this.connected=!1},M.prototype.getTitle=function(){return this.title},M.prototype.getValue=function(){return this.value},M.prototype.setValueRange=function(t,e){if(!this.widthFixed&&void 0!==this.value){var i=(this.widthMax-this.widthMin)/(e-t);this.width=(this.value-t)*i+this.widthMin}},M.prototype.draw=function(){throw"Method draw not initialized in edge"},M.prototype.isOverlappingWith=function(t){var e=10,i=this.from.x,n=this.from.y,s=this.to.x,o=this.to.y,r=t.left,a=t.top,h=M._dist(i,n,s,o,r,a);return e>h},M.prototype._drawLine=function(t){t.strokeStyle=this.color,t.lineWidth=this._getLineWidth();var e;if(this.from!=this.to)this._line(t),this.label&&(e=this._pointOnLine(.5),this._label(t,this.label,e.x,e.y));else{var i,n,s=this.length/4,o=this.from;o.width||o.resize(t),o.width>o.height?(i=o.x+o.width/2,n=o.y-s):(i=o.x+s,n=o.y-o.height/2),this._circle(t,i,n,s),e=this._pointOnCircle(i,n,s,.5),this._label(t,this.label,e.x,e.y)}},M.prototype._getLineWidth=function(){return this.from.selected||this.to.selected?Math.min(2*this.width,this.widthMax):this.width},M.prototype._line=function(t){t.beginPath(),t.moveTo(this.from.x,this.from.y),t.lineTo(this.to.x,this.to.y),t.stroke()},M.prototype._circle=function(t,e,i,n){t.beginPath(),t.arc(e,i,n,0,2*Math.PI,!1),t.stroke()},M.prototype._label=function(t,e,i,n){if(e){t.font=(this.from.selected||this.to.selected?"bold ":"")+this.fontSize+"px "+this.fontFace,t.fillStyle="white";var s=t.measureText(e).width,o=this.fontSize,r=i-s/2,a=n-o/2;t.fillRect(r,a,s,o),t.fillStyle=this.fontColor||"black",t.textAlign="left",t.textBaseline="top",t.fillText(e,r,a)}},M.prototype._drawDashLine=function(t){if(t.strokeStyle=this.color,t.lineWidth=this._getLineWidth(),t.beginPath(),t.lineCap="round",void 0!=this.dash.altLength?t.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]):void 0!=this.dash.length&&void 0!=this.dash.gap?t.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dash.length,this.dash.gap]):(t.moveTo(this.from.x,this.from.y),t.lineTo(this.to.x,this.to.y)),t.stroke(),this.label){var e=this._pointOnLine(.5);this._label(t,this.label,e.x,e.y)}},M.prototype._pointOnLine=function(t){return{x:(1-t)*this.from.x+t*this.to.x,y:(1-t)*this.from.y+t*this.to.y}},M.prototype._pointOnCircle=function(t,e,i,n){var s=2*(n-3/8)*Math.PI;return{x:t+i*Math.cos(s),y:e-i*Math.sin(s)}},M.prototype._drawArrowCenter=function(t){var e;if(t.strokeStyle=this.color,t.fillStyle=this.color,t.lineWidth=this._getLineWidth(),this.from!=this.to){this._line(t);var i=Math.atan2(this.to.y-this.from.y,this.to.x-this.from.x),n=10+5*this.width;e=this._pointOnLine(.5),t.arrow(e.x,e.y,i,n),t.fill(),t.stroke(),this.label&&(e=this._pointOnLine(.5),this._label(t,this.label,e.x,e.y))}else{var s,o,r=this.length/4,a=this.from;a.width||a.resize(t),a.width>a.height?(s=a.x+a.width/2,o=a.y-r):(s=a.x+r,o=a.y-a.height/2),this._circle(t,s,o,r);var i=.2*Math.PI,n=10+5*this.width;e=this._pointOnCircle(s,o,r,.5),t.arrow(e.x,e.y,i,n),t.fill(),t.stroke(),this.label&&(e=this._pointOnCircle(s,o,r,.5),this._label(t,this.label,e.x,e.y))}},M.prototype._drawArrow=function(t){t.strokeStyle=this.color,t.fillStyle=this.color,t.lineWidth=this._getLineWidth();var e,i;if(this.from!=this.to){e=Math.atan2(this.to.y-this.from.y,this.to.x-this.from.x);var n=this.to.x-this.from.x,s=this.to.y-this.from.y,o=Math.sqrt(n*n+s*s),r=this.from.distanceToBorder(t,e+Math.PI),a=(o-r)/o,h=a*this.from.x+(1-a)*this.to.x,d=a*this.from.y+(1-a)*this.to.y,l=this.to.distanceToBorder(t,e),p=(o-l)/o,u=(1-p)*this.from.x+p*this.to.x,c=(1-p)*this.from.y+p*this.to.y;if(t.beginPath(),t.moveTo(h,d),t.lineTo(u,c),t.stroke(),i=10+5*this.width,t.arrow(u,c,e,i),t.fill(),t.stroke(),this.label){var f=this._pointOnLine(.5);this._label(t,this.label,f.x,f.y)}}else{var m,g,v,y=this.from,w=this.length/4;y.width||y.resize(t),y.width>y.height?(m=y.x+y.width/2,g=y.y-w,v={x:m,y:y.y,angle:.9*Math.PI}):(m=y.x+w,g=y.y-y.height/2,v={x:y.x,y:g,angle:.6*Math.PI}),t.beginPath(),t.arc(m,g,w,0,2*Math.PI,!1),t.stroke(),i=10+5*this.width,t.arrow(v.x,v.y,v.angle,i),t.fill(),t.stroke(),this.label&&(f=this._pointOnCircle(m,g,w,.5),this._label(t,this.label,f.x,f.y))}},M._dist=function(t,e,i,n,s,o){var r=i-t,a=n-e,h=r*r+a*a,d=((s-t)*r+(o-e)*a)/h;d>1?d=1:0>d&&(d=0);var l=t+d*r,p=e+d*a,u=l-s,c=p-o;return Math.sqrt(u*u+c*c)},D.prototype.setPosition=function(t,e){this.x=parseInt(t),this.y=parseInt(e)},D.prototype.setText=function(t){this.frame.innerHTML=t},D.prototype.show=function(t){if(void 0===t&&(t=!0),t){var e=this.frame.clientHeight,i=this.frame.clientWidth,n=this.frame.parentNode.clientHeight,s=this.frame.parentNode.clientWidth,o=this.y-e;o+e+this.padding>n&&(o=n-e-this.padding),os&&(r=s-i-this.padding),r0?s[s.length-1]:null},C.prototype._getPointer=function(t){return{x:t.pageX-P.util.getAbsoluteLeft(this.frame.canvas),y:t.pageY-P.util.getAbsoluteTop(this.frame.canvas)}},C.prototype._onTouch=function(t){this.drag.pointer=this._getPointer(t.gesture.touches[0]),this.drag.pinched=!1,this.pinch.scale=this._getScale()},C.prototype._onDragStart=function(){var t=this.drag;t.selection=[],t.translation=this._getTranslation(),t.nodeId=this._getNodeAt(t.pointer);var e=this.nodes[t.nodeId];if(e){e.isSelected()||this._selectNodes([t.nodeId]);var i=this;this.selection.forEach(function(e){var n=i.nodes[e];if(n){var s={id:e,node:n,x:n.x,y:n.y,xFixed:n.xFixed,yFixed:n.yFixed};n.xFixed=!0,n.yFixed=!0,t.selection.push(s)}})}},C.prototype._onDrag=function(t){if(!this.drag.pinched){var e=this._getPointer(t.gesture.touches[0]),i=this,n=this.drag,s=n.selection;if(s&&s.length){var o=e.x-n.pointer.x,r=e.y-n.pointer.y;s.forEach(function(t){var e=t.node;t.xFixed||(e.x=i._canvasToX(i._xToCanvas(t.x)+o)),t.yFixed||(e.y=i._canvasToY(i._yToCanvas(t.y)+r))}),this.moving||(this.moving=!0,this.start())}else{var a=e.x-this.drag.pointer.x,h=e.y-this.drag.pointer.y;this._setTranslation(this.drag.translation.x+a,this.drag.translation.y+h),this._redraw(),this.moved=!0}}},C.prototype._onDragEnd=function(){var t=this.drag.selection;t&&t.forEach(function(t){t.node.xFixed=t.xFixed,t.node.yFixed=t.yFixed})},C.prototype._onTap=function(t){var e=this._getPointer(t.gesture.touches[0]),i=this._getNodeAt(e),n=this.nodes[i];n?(this._selectNodes([i]),this.moving||this._redraw()):(this._unselectNodes(),this._redraw())},C.prototype._onHold=function(t){var e=this._getPointer(t.gesture.touches[0]),i=this._getNodeAt(e),n=this.nodes[i];if(n){if(n.isSelected())this._unselectNodes([i]);else{var s=!0;this._selectNodes([i],s)}this.moving||this._redraw()}},C.prototype._onPinch=function(t){var e=this._getPointer(t.gesture.center);this.drag.pinched=!0,"scale"in this.pinch||(this.pinch.scale=1);var i=this.pinch.scale*t.gesture.scale;this._zoom(i,e)},C.prototype._zoom=function(t,e){var i=this._getScale();.01>t&&(t=.01),t>10&&(t=10);var n=this._getTranslation(),s=t/i,o=(1-s)*e.x+n.x*s,r=(1-s)*e.y+n.y*s;return this._setScale(t),this._setTranslation(o,r),this._redraw(),t},C.prototype._onMouseWheel=function(t){var e=0;if(t.wheelDelta?e=t.wheelDelta/120:t.detail&&(e=-t.detail/3),e){"mouswheelScale"in this.pinch||(this.pinch.mouswheelScale=1);var i=this.pinch.mouswheelScale,n=e/10;0>e&&(n/=1-n),i*=1+n;var s=O.event.collectEventData(this,"scroll",t),o=this._getPointer(s.center);i=this._zoom(i,o),this.pinch.mouswheelScale=i}t.preventDefault()},C.prototype._onMouseMoveTitle=function(t){var e=O.event.collectEventData(this,"mousemove",t),i=this._getPointer(e.center);this.popupNode&&this._checkHidePopup(i);var n=this,s=function(){n._checkShowPopup(i)};this.popupTimer&&clearInterval(this.popupTimer),this.leftButtonDown||(this.popupTimer=setTimeout(s,300))},C.prototype._checkShowPopup=function(t){var e,i={left:this._canvasToX(t.x),top:this._canvasToY(t.y),right:this._canvasToX(t.x),bottom:this._canvasToY(t.y)},n=this.popupNode;if(void 0==this.popupNode){var s=this.nodes;for(e in s)if(s.hasOwnProperty(e)){var o=s[e];if(void 0!=o.getTitle()&&o.isOverlappingWith(i)){this.popupNode=o;break}}}if(void 0==this.popupNode){var r=this.edges;for(e in r)if(r.hasOwnProperty(e)){var a=r[e];if(a.connected&&void 0!=a.getTitle()&&a.isOverlappingWith(i)){this.popupNode=a;break}}}if(this.popupNode){if(this.popupNode!=n){var h=this;h.popup||(h.popup=new D(h.frame)),h.popup.setPosition(t.x-3,t.y-3),h.popup.setText(h.popupNode.getTitle()),h.popup.show()}}else this.popup&&this.popup.hide()},C.prototype._checkHidePopup=function(t){this.popupNode&&this._getNodeAt(t)||(this.popupNode=void 0,this.popup&&this.popup.hide())},C.prototype._unselectNodes=function(t,e){var i,n,s,o=!1;if(t)for(i=0,n=t.length;n>i;i++){s=t[i],this.nodes[s].unselect();for(var r=0;ri;i++)s=this.selection[i],this.nodes[s].unselect(),o=!0;this.selection=[]}return!o||1!=e&&void 0!=e||this._trigger("select"),o},C.prototype._selectNodes=function(t,e){var i,n,s=!1,o=!0;if(t.length!=this.selection.length)o=!1;else for(i=0,n=Math.min(t.length,this.selection.length);n>i;i++)if(t[i]!=this.selection[i]){o=!1;break}if(o)return s;if(void 0==e||0==e){var r=!1;s=this._unselectNodes(void 0,r)}for(i=0,n=t.length;n>i;i++){var a=t[i],h=-1!=this.selection.indexOf(a);h||(this.nodes[a].select(),this.selection.push(a),s=!0)}return s&&this._trigger("select"),s},C.prototype._getNodesOverlappingWith=function(t){var e=this.nodes,i=[];for(var n in e)e.hasOwnProperty(n)&&e[n].isOverlappingWith(t)&&i.push(n);return i},C.prototype.getSelection=function(){return this.selection.concat([])},C.prototype.setSelection=function(t){var e,i,n;if(!t||void 0==t.length)throw"Selection must be an array with ids";for(e=0,i=this.selection.length;i>e;e++)n=this.selection[e],this.nodes[n].unselect();for(this.selection=[],e=0,i=t.length;i>e;e++){n=t[e];var s=this.nodes[n];if(!s)throw new RangeError('Node with id "'+n+'" not found');s.select(),this.selection.push(n)}this.redraw()},C.prototype._updateSelection=function(){for(var t=0;ti;i++)for(var s=t[i],o=s.edges,r=0,a=o.length;a>r;r++){var h=o[r],d=null;h.from==s?d=h.to:h.to==s&&(d=h.from);var l,p;if(d)for(l=0,p=t.length;p>l;l++)if(t[l]==d){d=null;break}if(d)for(l=0,p=e.length;p>l;l++)if(e[l]==d){d=null;break}d&&e.push(d)}return e}void 0==t&&(t=1);var i=[],n=this.nodes;for(var s in n)if(n.hasOwnProperty(s)){for(var o=[n[s]],r=0;t>r;r++)o=o.concat(e(o));i.push(o)}for(var a=[],h=0,d=i.length;d>h;h++)a.push(i[h].length);return a},C.prototype.setSize=function(t,e){this.frame.style.width=t,this.frame.style.height=e,this.frame.canvas.style.width="100%",this.frame.canvas.style.height="100%",this.frame.canvas.width=this.frame.canvas.clientWidth,this.frame.canvas.height=this.frame.canvas.clientHeight},C.prototype._setNodes=function(t){var e=this.nodesData;if(t instanceof o||t instanceof r)this.nodesData=t;else if(t instanceof Array)this.nodesData=new o,this.nodesData.add(t);else{if(t)throw new TypeError("Array or DataSet expected");this.nodesData=new o}if(e&&k.forEach(this.nodesListeners,function(t,i){e.unsubscribe(i,t)}),this.nodes={},this.nodesData){var i=this;k.forEach(this.nodesListeners,function(t,e){i.nodesData.subscribe(e,t)});var n=this.nodesData.getIds();this._addNodes(n)}this._updateSelection()},C.prototype._addNodes=function(t){for(var e,i=0,n=t.length;n>i;i++){e=t[i];var s=this.nodesData.get(e),o=new S(s,this.images,this.groups,this.constants);if(this.nodes[e]=o,!o.isFixed()){var r=2*this.constants.edges.length,a=t.length,h=2*Math.PI*(i/a);o.x=r*Math.cos(h),o.y=r*Math.sin(h),this.moving=!0}}this._reconnectEdges(),this._updateValueRange(this.nodes)},C.prototype._updateNodes=function(t){for(var e=this.nodes,i=this.nodesData,n=0,s=t.length;s>n;n++){var o=t[n],r=e[o],a=i.get(o);r?r.setProperties(a,this.constants):(r=new S(properties,this.images,this.groups,this.constants),e[o]=r,r.isFixed()||(this.moving=!0))}this._reconnectEdges(),this._updateValueRange(e)},C.prototype._removeNodes=function(t){for(var e=this.nodes,i=0,n=t.length;n>i;i++){var s=t[i];delete e[s]}this._reconnectEdges(),this._updateSelection(),this._updateValueRange(e)},C.prototype._setEdges=function(t){var e=this.edgesData;if(t instanceof o||t instanceof r)this.edgesData=t;else if(t instanceof Array)this.edgesData=new o,this.edgesData.add(t);else{if(t)throw new TypeError("Array or DataSet expected");this.edgesData=new o}if(e&&k.forEach(this.edgesListeners,function(t,i){e.unsubscribe(i,t)}),this.edges={},this.edgesData){var i=this;k.forEach(this.edgesListeners,function(t,e){i.edgesData.subscribe(e,t)});var n=this.edgesData.getIds();this._addEdges(n)}this._reconnectEdges()},C.prototype._addEdges=function(t){for(var e=this.edges,i=this.edgesData,n=0,s=t.length;s>n;n++){var o=t[n],r=e[o];r&&r.disconnect();var a=i.get(o);e[o]=new M(a,this,this.constants)}this.moving=!0,this._updateValueRange(e)},C.prototype._updateEdges=function(t){for(var e=this.edges,i=this.edgesData,n=0,s=t.length;s>n;n++){var o=t[n],r=i.get(o),a=e[o];a?(a.disconnect(),a.setProperties(r,this.constants),a.connect()):(a=new M(r,this,this.constants),this.edges[o]=a)}this.moving=!0,this._updateValueRange(e)},C.prototype._removeEdges=function(t){for(var e=this.edges,i=0,n=t.length;n>i;i++){var s=t[i],o=e[s];o&&(o.disconnect(),delete e[s])}this.moving=!0,this._updateValueRange(e)},C.prototype._reconnectEdges=function(){var t,e=this.nodes,i=this.edges;for(t in e)e.hasOwnProperty(t)&&(e[t].edges=[]);for(t in i)if(i.hasOwnProperty(t)){var n=i[t];n.from=null,n.to=null,n.connect()}},C.prototype._updateValueRange=function(t){var e,i=void 0,n=void 0;for(e in t)if(t.hasOwnProperty(e)){var s=t[e].getValue();void 0!==s&&(i=void 0===i?s:Math.min(s,i),n=void 0===n?s:Math.max(s,n))}if(void 0!==i&&void 0!==n)for(e in t)t.hasOwnProperty(e)&&t[e].setValueRange(i,n)},C.prototype.redraw=function(){this.setSize(this.width,this.height),this._redraw()},C.prototype._redraw=function(){var t=this.frame.canvas.getContext("2d"),e=this.frame.canvas.width,i=this.frame.canvas.height;t.clearRect(0,0,e,i),t.save(),t.translate(this.translation.x,this.translation.y),t.scale(this.scale,this.scale),this._drawEdges(t),this._drawNodes(t),t.restore()},C.prototype._setTranslation=function(t,e){void 0===this.translation&&(this.translation={x:0,y:0}),void 0!==t&&(this.translation.x=t),void 0!==e&&(this.translation.y=e)},C.prototype._getTranslation=function(){return{x:this.translation.x,y:this.translation.y}},C.prototype._setScale=function(t){this.scale=t},C.prototype._getScale=function(){return this.scale},C.prototype._canvasToX=function(t){return(t-this.translation.x)/this.scale},C.prototype._xToCanvas=function(t){return t*this.scale+this.translation.x},C.prototype._canvasToY=function(t){return(t-this.translation.y)/this.scale},C.prototype._yToCanvas=function(t){return t*this.scale+this.translation.y},C.prototype._drawNodes=function(t){var e=this.nodes,i=[];for(var n in e)e.hasOwnProperty(n)&&(e[n].isSelected()?i.push(n):e[n].draw(t));for(var s=0,o=i.length;o>s;s++)e[i[s]].draw(t)},C.prototype._drawEdges=function(t){var e=this.edges;for(var i in e)if(e.hasOwnProperty(i)){var n=e[i];n.connected&&e[i].draw(t)}},C.prototype._doStabilize=function(){new Date;for(var t=0,e=this.constants.minVelocity,i=!1;!i&&tr;r++)s(n[r]);return s}({1:[function(t,e){(function(t,i){"use strict";function n(){if(!s.READY){s.event.determineEventTypes();for(var t in s.gestures)s.gestures.hasOwnProperty(t)&&s.detection.register(s.gestures[t]);s.event.onTouch(s.DOCUMENT,s.EVENT_MOVE,s.detection.detect),s.event.onTouch(s.DOCUMENT,s.EVENT_END,s.detection.detect),s.READY=!0}}var s=function(t,e){return new s.Instance(t,e||{})};s.defaults={stop_browser_behavior:{userSelect:"none",touchAction:"none",touchCallout:"none",contentZooming:"none",userDrag:"none",tapHighlightColor:"rgba(0,0,0,0)"}},s.HAS_POINTEREVENTS=navigator.pointerEnabled||navigator.msPointerEnabled,s.HAS_TOUCHEVENTS="ontouchstart"in t,s.MOBILE_REGEX=/mobile|tablet|ip(ad|hone|od)|android/i,s.NO_MOUSEEVENTS=s.HAS_TOUCHEVENTS&&navigator.userAgent.match(s.MOBILE_REGEX),s.EVENT_TYPES={},s.DIRECTION_DOWN="down",s.DIRECTION_LEFT="left",s.DIRECTION_UP="up",s.DIRECTION_RIGHT="right",s.POINTER_MOUSE="mouse",s.POINTER_TOUCH="touch",s.POINTER_PEN="pen",s.EVENT_START="start",s.EVENT_MOVE="move",s.EVENT_END="end",s.DOCUMENT=document,s.plugins={},s.READY=!1,s.Instance=function(t,e){var i=this;return n(),this.element=t,this.enabled=!0,this.options=s.utils.extend(s.utils.extend({},s.defaults),e||{}),this.options.stop_browser_behavior&&s.utils.stopDefaultBrowserBehavior(this.element,this.options.stop_browser_behavior),s.event.onTouch(t,s.EVENT_START,function(t){i.enabled&&s.detection.startDetect(i,t)}),this},s.Instance.prototype={on:function(t,e){for(var i=t.split(" "),n=0;i.length>n;n++)this.element.addEventListener(i[n],e,!1);return this},off:function(t,e){for(var i=t.split(" "),n=0;i.length>n;n++)this.element.removeEventListener(i[n],e,!1);return this},trigger:function(t,e){var i=s.DOCUMENT.createEvent("Event");i.initEvent(t,!0,!0),i.gesture=e;var n=this.element;return s.utils.hasParent(e.target,n)&&(n=e.target),n.dispatchEvent(i),this},enable:function(t){return this.enabled=t,this}};var o=null,r=!1,a=!1;s.event={bindDom:function(t,e,i){for(var n=e.split(" "),s=0;n.length>s;s++)t.addEventListener(n[s],i,!1)},onTouch:function(t,e,i){var n=this;this.bindDom(t,s.EVENT_TYPES[e],function(h){var d=h.type.toLowerCase();if(!d.match(/mouse/)||!a){(d.match(/touch/)||d.match(/pointerdown/)||d.match(/mouse/)&&1===h.which)&&(r=!0),d.match(/touch|pointer/)&&(a=!0);var l=0;r&&(s.HAS_POINTEREVENTS&&e!=s.EVENT_END?l=s.PointerEvent.updatePointer(e,h):d.match(/touch/)?l=h.touches.length:a||(l=d.match(/up/)?0:1),l>0&&e==s.EVENT_END?e=s.EVENT_MOVE:l||(e=s.EVENT_END),l||null===o?o=h:h=o,i.call(s.detection,n.collectEventData(t,e,h)),s.HAS_POINTEREVENTS&&e==s.EVENT_END&&(l=s.PointerEvent.updatePointer(e,h))),l||(o=null,r=!1,a=!1,s.PointerEvent.reset())}})},determineEventTypes:function(){var t;t=s.HAS_POINTEREVENTS?s.PointerEvent.getEvents():s.NO_MOUSEEVENTS?["touchstart","touchmove","touchend touchcancel"]:["touchstart mousedown","touchmove mousemove","touchend touchcancel mouseup"],s.EVENT_TYPES[s.EVENT_START]=t[0],s.EVENT_TYPES[s.EVENT_MOVE]=t[1],s.EVENT_TYPES[s.EVENT_END]=t[2]},getTouchList:function(t){return s.HAS_POINTEREVENTS?s.PointerEvent.getTouchList():t.touches?t.touches:[{identifier:1,pageX:t.pageX,pageY:t.pageY,target:t.target}]},collectEventData:function(t,e,i){var n=this.getTouchList(i,e),o=s.POINTER_TOUCH;return(i.type.match(/mouse/)||s.PointerEvent.matchType(s.POINTER_MOUSE,i))&&(o=s.POINTER_MOUSE),{center:s.utils.getCenter(n),timeStamp:(new Date).getTime(),target:i.target,touches:n,eventType:e,pointerType:o,srcEvent:i,preventDefault:function(){this.srcEvent.preventManipulation&&this.srcEvent.preventManipulation(),this.srcEvent.preventDefault&&this.srcEvent.preventDefault()},stopPropagation:function(){this.srcEvent.stopPropagation()},stopDetect:function(){return s.detection.stopDetect()}}}},s.PointerEvent={pointers:{},getTouchList:function(){var t=this,e=[];return Object.keys(t.pointers).sort().forEach(function(i){e.push(t.pointers[i])}),e},updatePointer:function(t,e){return t==s.EVENT_END?this.pointers={}:(e.identifier=e.pointerId,this.pointers[e.pointerId]=e),Object.keys(this.pointers).length},matchType:function(t,e){if(!e.pointerType)return!1;var i={};return i[s.POINTER_MOUSE]=e.pointerType==e.MSPOINTER_TYPE_MOUSE||e.pointerType==s.POINTER_MOUSE,i[s.POINTER_TOUCH]=e.pointerType==e.MSPOINTER_TYPE_TOUCH||e.pointerType==s.POINTER_TOUCH,i[s.POINTER_PEN]=e.pointerType==e.MSPOINTER_TYPE_PEN||e.pointerType==s.POINTER_PEN,i[t]},getEvents:function(){return["pointerdown MSPointerDown","pointermove MSPointerMove","pointerup pointercancel MSPointerUp MSPointerCancel"]},reset:function(){this.pointers={}}},s.utils={extend:function(t,e,n){for(var s in e)t[s]!==i&&n||(t[s]=e[s]);return t},hasParent:function(t,e){for(;t;){if(t==e)return!0;t=t.parentNode}return!1},getCenter:function(t){for(var e=[],i=[],n=0,s=t.length;s>n;n++)e.push(t[n].pageX),i.push(t[n].pageY);return{pageX:(Math.min.apply(Math,e)+Math.max.apply(Math,e))/2,pageY:(Math.min.apply(Math,i)+Math.max.apply(Math,i))/2}},getVelocity:function(t,e,i){return{x:Math.abs(e/t)||0,y:Math.abs(i/t)||0}},getAngle:function(t,e){var i=e.pageY-t.pageY,n=e.pageX-t.pageX;return 180*Math.atan2(i,n)/Math.PI},getDirection:function(t,e){var i=Math.abs(t.pageX-e.pageX),n=Math.abs(t.pageY-e.pageY);return i>=n?t.pageX-e.pageX>0?s.DIRECTION_LEFT:s.DIRECTION_RIGHT:t.pageY-e.pageY>0?s.DIRECTION_UP:s.DIRECTION_DOWN},getDistance:function(t,e){var i=e.pageX-t.pageX,n=e.pageY-t.pageY;return Math.sqrt(i*i+n*n)},getScale:function(t,e){return t.length>=2&&e.length>=2?this.getDistance(e[0],e[1])/this.getDistance(t[0],t[1]):1},getRotation:function(t,e){return t.length>=2&&e.length>=2?this.getAngle(e[1],e[0])-this.getAngle(t[1],t[0]):0},isVertical:function(t){return t==s.DIRECTION_UP||t==s.DIRECTION_DOWN},stopDefaultBrowserBehavior:function(t,e){var i,n=["webkit","khtml","moz","ms","o",""];if(e&&t.style){for(var s=0;n.length>s;s++)for(var o in e)e.hasOwnProperty(o)&&(i=o,n[s]&&(i=n[s]+i.substring(0,1).toUpperCase()+i.substring(1)),t.style[i]=e[o]);"none"==e.userSelect&&(t.onselectstart=function(){return!1})}}},s.detection={gestures:[],current:null,previous:null,stopped:!1,startDetect:function(t,e){this.current||(this.stopped=!1,this.current={inst:t,startEvent:s.utils.extend({},e),lastEvent:!1,name:""},this.detect(e))},detect:function(t){if(this.current&&!this.stopped){t=this.extendEventData(t);for(var e=this.current.inst.options,i=0,n=this.gestures.length;n>i;i++){var o=this.gestures[i];if(!this.stopped&&e[o.name]!==!1&&o.handler.call(o,t,this.current.inst)===!1){this.stopDetect();break}}return this.current&&(this.current.lastEvent=t),t.eventType==s.EVENT_END&&!t.touches.length-1&&this.stopDetect(),t}},stopDetect:function(){this.previous=s.utils.extend({},this.current),this.current=null,this.stopped=!0},extendEventData:function(t){var e=this.current.startEvent;if(e&&(t.touches.length!=e.touches.length||t.touches===e.touches)){e.touches=[];for(var i=0,n=t.touches.length;n>i;i++)e.touches.push(s.utils.extend({},t.touches[i]))}var o=t.timeStamp-e.timeStamp,r=t.center.pageX-e.center.pageX,a=t.center.pageY-e.center.pageY,h=s.utils.getVelocity(o,r,a);return s.utils.extend(t,{deltaTime:o,deltaX:r,deltaY:a,velocityX:h.x,velocityY:h.y,distance:s.utils.getDistance(e.center,t.center),angle:s.utils.getAngle(e.center,t.center),direction:s.utils.getDirection(e.center,t.center),scale:s.utils.getScale(e.touches,t.touches),rotation:s.utils.getRotation(e.touches,t.touches),startEvent:e}),t},register:function(t){var e=t.defaults||{};return e[t.name]===i&&(e[t.name]=!0),s.utils.extend(s.defaults,e,!0),t.index=t.index||1e3,this.gestures.push(t),this.gestures.sort(function(t,e){return t.indexe.index?1:0}),this.gestures}},s.gestures=s.gestures||{},s.gestures.Hold={name:"hold",index:10,defaults:{hold_timeout:500,hold_threshold:1},timer:null,handler:function(t,e){switch(t.eventType){case s.EVENT_START:clearTimeout(this.timer),s.detection.current.name=this.name,this.timer=setTimeout(function(){"hold"==s.detection.current.name&&e.trigger("hold",t)},e.options.hold_timeout);break;case s.EVENT_MOVE:t.distance>e.options.hold_threshold&&clearTimeout(this.timer);break;case s.EVENT_END:clearTimeout(this.timer)}}},s.gestures.Tap={name:"tap",index:100,defaults:{tap_max_touchtime:250,tap_max_distance:10,tap_always:!0,doubletap_distance:20,doubletap_interval:300},handler:function(t,e){if(t.eventType==s.EVENT_END){var i=s.detection.previous,n=!1;if(t.deltaTime>e.options.tap_max_touchtime||t.distance>e.options.tap_max_distance)return;i&&"tap"==i.name&&t.timeStamp-i.lastEvent.timeStamp0&&t.touches.length>e.options.swipe_max_touches)return;(t.velocityX>e.options.swipe_velocity||t.velocityY>e.options.swipe_velocity)&&(e.trigger(this.name,t),e.trigger(this.name+t.direction,t))}}},s.gestures.Drag={name:"drag",index:50,defaults:{drag_min_distance:10,drag_max_touches:1,drag_block_horizontal:!1,drag_block_vertical:!1,drag_lock_to_axis:!1,drag_lock_min_distance:25},triggered:!1,handler:function(t,e){if(s.detection.current.name!=this.name&&this.triggered)return e.trigger(this.name+"end",t),this.triggered=!1,i;if(!(e.options.drag_max_touches>0&&t.touches.length>e.options.drag_max_touches))switch(t.eventType){case s.EVENT_START:this.triggered=!1;break;case s.EVENT_MOVE:if(t.distancet.deltaY?s.DIRECTION_UP:s.DIRECTION_DOWN:0>t.deltaX?s.DIRECTION_LEFT:s.DIRECTION_RIGHT),this.triggered||(e.trigger(this.name+"start",t),this.triggered=!0),e.trigger(this.name,t),e.trigger(this.name+t.direction,t),(e.options.drag_block_vertical&&s.utils.isVertical(t.direction)||e.options.drag_block_horizontal&&!s.utils.isVertical(t.direction))&&t.preventDefault();break;case s.EVENT_END:this.triggered&&e.trigger(this.name+"end",t),this.triggered=!1}}},s.gestures.Transform={name:"transform",index:45,defaults:{transform_min_scale:.01,transform_min_rotation:1,transform_always_block:!1},triggered:!1,handler:function(t,e){if(s.detection.current.name!=this.name&&this.triggered)return e.trigger(this.name+"end",t),this.triggered=!1,i;if(!(2>t.touches.length))switch(e.options.transform_always_block&&t.preventDefault(),t.eventType){case s.EVENT_START:this.triggered=!1;break;case s.EVENT_MOVE:var n=Math.abs(1-t.scale),o=Math.abs(t.rotation);if(e.options.transform_min_scale>n&&e.options.transform_min_rotation>o)return;s.detection.current.name=this.name,this.triggered||(e.trigger(this.name+"start",t),this.triggered=!0),e.trigger(this.name,t),o>e.options.transform_min_rotation&&e.trigger("rotate",t),n>e.options.transform_min_scale&&(e.trigger("pinch",t),e.trigger("pinch"+(1>t.scale?"in":"out"),t));break;case s.EVENT_END:this.triggered&&e.trigger(this.name+"end",t),this.triggered=!1}}},s.gestures.Touch={name:"touch",index:-1/0,defaults:{prevent_default:!1,prevent_mouseevents:!1},handler:function(t,e){return e.options.prevent_mouseevents&&t.pointerType==s.POINTER_MOUSE?(t.stopDetect(),i):(e.options.prevent_default&&t.preventDefault(),t.eventType==s.EVENT_START&&e.trigger(this.name,t),i)}},s.gestures.Release={name:"release",index:1/0,handler:function(t,e){t.eventType==s.EVENT_END&&e.trigger(this.name,t)}},"object"==typeof e&&"object"==typeof e.exports?e.exports=s:(t.Hammer=s,"function"==typeof t.define&&t.define.amd&&t.define("hammer",[],function(){return s}))})(this)},{}],2:[function(e,i){(function(n){function s(t,e){return function(i){return p(t.call(this,i),e)}}function o(t,e){return function(i){return this.lang().ordinal(t.call(this,i),e)}}function r(){}function a(t){d(this,t)}function h(t){var e=t.years||t.year||t.y||0,i=t.months||t.month||t.M||0,n=t.weeks||t.week||t.w||0,s=t.days||t.day||t.d||0,o=t.hours||t.hour||t.h||0,r=t.minutes||t.minute||t.m||0,a=t.seconds||t.second||t.s||0,h=t.milliseconds||t.millisecond||t.ms||0;this._input=t,this._milliseconds=+h+1e3*a+6e4*r+36e5*o,this._days=+s+7*n,this._months=+i+12*e,this._data={},this._bubble()}function d(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return t}function l(t){return 0>t?Math.ceil(t):Math.floor(t)}function p(t,e){for(var i=t+"";e>i.length;)i="0"+i;return i}function c(t,e,i,n){var s,o,r=e._milliseconds,a=e._days,h=e._months;r&&t._d.setTime(+t._d+r*i),(a||h)&&(s=t.minute(),o=t.hour()),a&&t.date(t.date()+a*i),h&&t.month(t.month()+h*i),r&&!n&&H.updateOffset(t),(a||h)&&(t.minute(s),t.hour(o))}function u(t){return"[object Array]"===Object.prototype.toString.call(t)}function f(t,e){var i,n=Math.min(t.length,e.length),s=Math.abs(t.length-e.length),o=0;for(i=0;n>i;i++)~~t[i]!==~~e[i]&&o++;return o+s}function m(t){return t?pe[t]||t.toLowerCase().replace(/(.)s$/,"$1"):t}function g(t,e){return e.abbr=t,V[t]||(V[t]=new r),V[t].set(e),V[t]}function v(t){delete V[t]}function y(t){if(!t)return H.fn._lang;if(!V[t]&&B)try{e("./lang/"+t)}catch(i){return H.fn._lang}return V[t]||H.fn._lang}function b(t){return t.match(/\[.*\]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function w(t){var e,i,n=t.match(X);for(e=0,i=n.length;i>e;e++)n[e]=me[n[e]]?me[n[e]]:b(n[e]);return function(s){var o="";for(e=0;i>e;e++)o+=n[e]instanceof Function?n[e].call(s,t):n[e];return o}}function _(t,e){return e=E(e,t.lang()),ce[e]||(ce[e]=w(e)),ce[e](t)}function E(t,e){function i(t){return e.longDateFormat(t)||t}for(var n=5;n--&&(Z.lastIndex=0,Z.test(t));)t=t.replace(Z,i);return t}function T(t,e){switch(t){case"DDDD":return Q;case"YYYY":return $;case"YYYYY":return te;case"S":case"SS":case"SSS":case"DDD":return J;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return ee;case"a":case"A":return y(e._l)._meridiemParse;case"X":return se;case"Z":case"ZZ":return ie;case"T":return ne;case"MM":case"DD":case"YY":case"HH":case"hh":case"mm":case"ss":case"M":case"D":case"d":case"H":case"h":case"m":case"s":return K;default:return RegExp(t.replace("\\",""))}}function x(t){var e=(ie.exec(t)||[])[0],i=(e+"").match(he)||["-",0,0],n=+(60*i[1])+~~i[2];return"+"===i[0]?-n:n}function S(t,e,i){var n,s=i._a;switch(t){case"M":case"MM":null!=e&&(s[1]=~~e-1);break;case"MMM":case"MMMM":n=y(i._l).monthsParse(e),null!=n?s[1]=n:i._isValid=!1;break;case"D":case"DD":null!=e&&(s[2]=~~e);break;case"DDD":case"DDDD":null!=e&&(s[1]=0,s[2]=~~e);break;case"YY":s[0]=~~e+(~~e>68?1900:2e3);break;case"YYYY":case"YYYYY":s[0]=~~e;break;case"a":case"A":i._isPm=y(i._l).isPM(e);break;case"H":case"HH":case"h":case"hh":s[3]=~~e;break;case"m":case"mm":s[4]=~~e;break;case"s":case"ss":s[5]=~~e;break;case"S":case"SS":case"SSS":s[6]=~~(1e3*("0."+e));break;case"X":i._d=new Date(1e3*parseFloat(e));break;case"Z":case"ZZ":i._useUTC=!0,i._tzm=x(e)}null==e&&(i._isValid=!1)}function D(t){var e,i,n,s=[];if(!t._d){for(n=C(t),e=0;3>e&&null==t._a[e];++e)t._a[e]=s[e]=n[e];for(;7>e;e++)t._a[e]=s[e]=null==t._a[e]?2===e?1:0:t._a[e];s[3]+=~~((t._tzm||0)/60),s[4]+=~~((t._tzm||0)%60),i=new Date(0),t._useUTC?(i.setUTCFullYear(s[0],s[1],s[2]),i.setUTCHours(s[3],s[4],s[5],s[6])):(i.setFullYear(s[0],s[1],s[2]),i.setHours(s[3],s[4],s[5],s[6])),t._d=i}}function M(t){var e=t._i;t._d||(t._a=[e.years||e.year||e.y,e.months||e.month||e.M,e.days||e.day||e.d,e.hours||e.hour||e.h,e.minutes||e.minute||e.m,e.seconds||e.second||e.s,e.milliseconds||e.millisecond||e.ms],D(t))}function C(t){var e=new Date;return t._useUTC?[e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()]:[e.getFullYear(),e.getMonth(),e.getDate()]}function O(t){var e,i,n,s=y(t._l),o=""+t._i;for(n=E(t._f,s).match(X),t._a=[],e=0;n.length>e;e++)i=(T(n[e],t).exec(o)||[])[0],i&&(o=o.slice(o.indexOf(i)+i.length)),me[n[e]]&&S(n[e],i,t);o&&(t._il=o),t._isPm&&12>t._a[3]&&(t._a[3]+=12),t._isPm===!1&&12===t._a[3]&&(t._a[3]=0),D(t)}function N(t){var e,i,n,s,o,r=99;for(s=0;t._f.length>s;s++)e=d({},t),e._f=t._f[s],O(e),i=new a(e),o=f(e._a,i.toArray()),i._il&&(o+=i._il.length),r>o&&(r=o,n=i);d(t,n)}function L(t){var e,i=t._i,n=oe.exec(i);if(n){for(t._f="YYYY-MM-DD"+(n[2]||" "),e=0;4>e;e++)if(ae[e][1].exec(i)){t._f+=ae[e][0];break}ie.exec(i)&&(t._f+=" Z"),O(t)}else t._d=new Date(i)}function I(t){var e=t._i,i=q.exec(e);e===n?t._d=new Date:i?t._d=new Date(+i[1]):"string"==typeof e?L(t):u(e)?(t._a=e.slice(0),D(t)):e instanceof Date?t._d=new Date(+e):"object"==typeof e?M(t):t._d=new Date(e)}function k(t,e,i,n,s){return s.relativeTime(e||1,!!i,t,n)}function A(t,e,i){var n=W(Math.abs(t)/1e3),s=W(n/60),o=W(s/60),r=W(o/24),a=W(r/365),h=45>n&&["s",n]||1===s&&["m"]||45>s&&["mm",s]||1===o&&["h"]||22>o&&["hh",o]||1===r&&["d"]||25>=r&&["dd",r]||45>=r&&["M"]||345>r&&["MM",W(r/30)]||1===a&&["y"]||["yy",a];return h[2]=e,h[3]=t>0,h[4]=i,k.apply({},h)}function P(t,e,i){var n,s=i-e,o=i-t.day();return o>s&&(o-=7),s-7>o&&(o+=7),n=H(t).add("d",o),{week:Math.ceil(n.dayOfYear()/7),year:n.year()}}function F(t){var e=t._i,i=t._f;return null===e||""===e?null:("string"==typeof e&&(t._i=e=y().preparse(e)),H.isMoment(e)?(t=d({},e),t._d=new Date(+e._d)):i?u(i)?N(t):O(t):I(t),new a(t))}function Y(t,e){H.fn[t]=H.fn[t+"s"]=function(t){var i=this._isUTC?"UTC":"";return null!=t?(this._d["set"+i+e](t),H.updateOffset(this),this):this._d["get"+i+e]()}}function R(t){H.duration.fn[t]=function(){return this._data[t]}}function z(t,e){H.duration.fn["as"+t]=function(){return+this/e}}for(var H,U,j="2.2.1",W=Math.round,V={},B=i!==n&&i.exports,q=/^\/?Date\((\-?\d+)/i,G=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)\:(\d+)\.?(\d{3})?/,X=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|SS?S?|X|zz?|ZZ?|.)/g,Z=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,K=/\d\d?/,J=/\d{1,3}/,Q=/\d{3}/,$=/\d{1,4}/,te=/[+\-]?\d{1,6}/,ee=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,ie=/Z|[\+\-]\d\d:?\d\d/i,ne=/T/i,se=/[\+\-]?\d+(\.\d{1,3})?/,oe=/^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/,re="YYYY-MM-DDTHH:mm:ssZ",ae=[["HH:mm:ss.S",/(T| )\d\d:\d\d:\d\d\.\d{1,3}/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],he=/([\+\-]|\d\d)/gi,de="Date|Hours|Minutes|Seconds|Milliseconds".split("|"),le={Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6},pe={ms:"millisecond",s:"second",m:"minute",h:"hour",d:"day",w:"week",W:"isoweek",M:"month",y:"year"},ce={},ue="DDD w W M D d".split(" "),fe="M D H h m s w W".split(" "),me={M:function(){return this.month()+1},MMM:function(t){return this.lang().monthsShort(this,t)},MMMM:function(t){return this.lang().months(this,t)},D:function(){return this.date()},DDD:function(){return this.dayOfYear()},d:function(){return this.day()},dd:function(t){return this.lang().weekdaysMin(this,t)},ddd:function(t){return this.lang().weekdaysShort(this,t)},dddd:function(t){return this.lang().weekdays(this,t)},w:function(){return this.week()},W:function(){return this.isoWeek()},YY:function(){return p(this.year()%100,2)},YYYY:function(){return p(this.year(),4)},YYYYY:function(){return p(this.year(),5)},gg:function(){return p(this.weekYear()%100,2)},gggg:function(){return this.weekYear()},ggggg:function(){return p(this.weekYear(),5)},GG:function(){return p(this.isoWeekYear()%100,2)},GGGG:function(){return this.isoWeekYear()},GGGGG:function(){return p(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return~~(this.milliseconds()/100)},SS:function(){return p(~~(this.milliseconds()/10),2)},SSS:function(){return p(this.milliseconds(),3)},Z:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+p(~~(t/60),2)+":"+p(~~t%60,2)},ZZ:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+p(~~(10*t/6),4)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()}};ue.length;)U=ue.pop(),me[U+"o"]=o(me[U],U);for(;fe.length;)U=fe.pop(),me[U+U]=s(me[U],2);for(me.DDDD=s(me.DDD,3),d(r.prototype,{set:function(t){var e,i;for(i in t)e=t[i],"function"==typeof e?this[i]=e:this["_"+i]=e},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(t){return this._months[t.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(t){return this._monthsShort[t.month()]},monthsParse:function(t){var e,i,n;for(this._monthsParse||(this._monthsParse=[]),e=0;12>e;e++)if(this._monthsParse[e]||(i=H.utc([2e3,e]),n="^"+this.months(i,"")+"|^"+this.monthsShort(i,""),this._monthsParse[e]=RegExp(n.replace(".",""),"i")),this._monthsParse[e].test(t))return e},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(t){return this._weekdays[t.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(t){return this._weekdaysShort[t.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(t){return this._weekdaysMin[t.day()]},weekdaysParse:function(t){var e,i,n;for(this._weekdaysParse||(this._weekdaysParse=[]),e=0;7>e;e++)if(this._weekdaysParse[e]||(i=H([2e3,1]).day(e),n="^"+this.weekdays(i,"")+"|^"+this.weekdaysShort(i,"")+"|^"+this.weekdaysMin(i,""),this._weekdaysParse[e]=RegExp(n.replace(".",""),"i")),this._weekdaysParse[e].test(t))return e},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(t){var e=this._longDateFormat[t];return!e&&this._longDateFormat[t.toUpperCase()]&&(e=this._longDateFormat[t.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t]=e),e},isPM:function(t){return"p"===(t+"").toLowerCase().charAt(0)},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(t,e,i){return t>11?i?"pm":"PM":i?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(t,e){var i=this._calendar[t];return"function"==typeof i?i.apply(e):i},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(t,e,i,n){var s=this._relativeTime[i];return"function"==typeof s?s(t,e,i,n):s.replace(/%d/i,t)},pastFuture:function(t,e){var i=this._relativeTime[t>0?"future":"past"];return"function"==typeof i?i(e):i.replace(/%s/i,e)},ordinal:function(t){return this._ordinal.replace("%d",t)},_ordinal:"%d",preparse:function(t){return t},postformat:function(t){return t},week:function(t){return P(t,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6}}),H=function(t,e,i){return F({_i:t,_f:e,_l:i,_isUTC:!1})},H.utc=function(t,e,i){return F({_useUTC:!0,_isUTC:!0,_l:i,_i:t,_f:e}).utc()},H.unix=function(t){return H(1e3*t)},H.duration=function(t,e){var i,n,s=H.isDuration(t),o="number"==typeof t,r=s?t._input:o?{}:t,a=G.exec(t);return o?e?r[e]=t:r.milliseconds=t:a&&(i="-"===a[1]?-1:1,r={y:0,d:~~a[2]*i,h:~~a[3]*i,m:~~a[4]*i,s:~~a[5]*i,ms:~~a[6]*i}),n=new h(r),s&&t.hasOwnProperty("_lang")&&(n._lang=t._lang),n},H.version=j,H.defaultFormat=re,H.updateOffset=function(){},H.lang=function(t,e){return t?(t=t.toLowerCase(),t=t.replace("_","-"),e?g(t,e):null===e?(v(t),t="en"):V[t]||y(t),H.duration.fn._lang=H.fn._lang=y(t),n):H.fn._lang._abbr},H.langData=function(t){return t&&t._lang&&t._lang._abbr&&(t=t._lang._abbr),y(t)},H.isMoment=function(t){return t instanceof a},H.isDuration=function(t){return t instanceof h},d(H.fn=a.prototype,{clone:function(){return H(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){return _(H(this).utc(),"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]")},toArray:function(){var t=this;return[t.year(),t.month(),t.date(),t.hours(),t.minutes(),t.seconds(),t.milliseconds()]},isValid:function(){return null==this._isValid&&(this._isValid=this._a?!f(this._a,(this._isUTC?H.utc(this._a):H(this._a)).toArray()):!isNaN(this._d.getTime())),!!this._isValid},invalidAt:function(){var t,e=this._a,i=(this._isUTC?H.utc(this._a):H(this._a)).toArray();for(t=6;t>=0&&e[t]===i[t];--t);return t},utc:function(){return this.zone(0)},local:function(){return this.zone(0),this._isUTC=!1,this},format:function(t){var e=_(this,t||H.defaultFormat);return this.lang().postformat(e)},add:function(t,e){var i;return i="string"==typeof t?H.duration(+e,t):H.duration(t,e),c(this,i,1),this},subtract:function(t,e){var i;return i="string"==typeof t?H.duration(+e,t):H.duration(t,e),c(this,i,-1),this},diff:function(t,e,i){var n,s,o=this._isUTC?H(t).zone(this._offset||0):H(t).local(),r=6e4*(this.zone()-o.zone());return e=m(e),"year"===e||"month"===e?(n=432e5*(this.daysInMonth()+o.daysInMonth()),s=12*(this.year()-o.year())+(this.month()-o.month()),s+=(this-H(this).startOf("month")-(o-H(o).startOf("month")))/n,s-=6e4*(this.zone()-H(this).startOf("month").zone()-(o.zone()-H(o).startOf("month").zone()))/n,"year"===e&&(s/=12)):(n=this-o,s="second"===e?n/1e3:"minute"===e?n/6e4:"hour"===e?n/36e5:"day"===e?(n-r)/864e5:"week"===e?(n-r)/6048e5:n),i?s:l(s)},from:function(t,e){return H.duration(this.diff(t)).lang(this.lang()._abbr).humanize(!e)},fromNow:function(t){return this.from(H(),t)},calendar:function(){var t=this.diff(H().zone(this.zone()).startOf("day"),"days",!0),e=-6>t?"sameElse":-1>t?"lastWeek":0>t?"lastDay":1>t?"sameDay":2>t?"nextDay":7>t?"nextWeek":"sameElse";return this.format(this.lang().calendar(e,this))},isLeapYear:function(){var t=this.year();return 0===t%4&&0!==t%100||0===t%400},isDST:function(){return this.zone()+H(t).startOf(e)},isBefore:function(t,e){return e=e!==n?e:"millisecond",+this.clone().startOf(e)<+H(t).startOf(e)},isSame:function(t,e){return e=e!==n?e:"millisecond",+this.clone().startOf(e)===+H(t).startOf(e)},min:function(t){return t=H.apply(null,arguments),this>t?this:t},max:function(t){return t=H.apply(null,arguments),t>this?this:t},zone:function(t){var e=this._offset||0;return null==t?this._isUTC?e:this._d.getTimezoneOffset():("string"==typeof t&&(t=x(t)),16>Math.abs(t)&&(t=60*t),this._offset=t,this._isUTC=!0,e!==t&&c(this,H.duration(e-t,"m"),1,!0),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},hasAlignedHourOffset:function(t){return t=t?H(t).zone():0,0===(this.zone()-t)%60},daysInMonth:function(){return H.utc([this.year(),this.month()+1,0]).date()},dayOfYear:function(t){var e=W((H(this).startOf("day")-H(this).startOf("year"))/864e5)+1;return null==t?e:this.add("d",t-e)},weekYear:function(t){var e=P(this,this.lang()._week.dow,this.lang()._week.doy).year;return null==t?e:this.add("y",t-e)},isoWeekYear:function(t){var e=P(this,1,4).year;return null==t?e:this.add("y",t-e)},week:function(t){var e=this.lang().week(this);return null==t?e:this.add("d",7*(t-e))},isoWeek:function(t){var e=P(this,1,4).week;return null==t?e:this.add("d",7*(t-e))},weekday:function(t){var e=(this._d.getDay()+7-this.lang()._week.dow)%7;return null==t?e:this.add("d",t-e)},isoWeekday:function(t){return null==t?this.day()||7:this.day(this.day()%7?t:t-7)},get:function(t){return t=m(t),this[t.toLowerCase()]()},set:function(t,e){t=m(t),this[t.toLowerCase()](e)},lang:function(t){return t===n?this._lang:(this._lang=y(t),this)}}),U=0;de.length>U;U++)Y(de[U].toLowerCase().replace(/s$/,""),de[U]);Y("year","FullYear"),H.fn.days=H.fn.day,H.fn.months=H.fn.month,H.fn.weeks=H.fn.week,H.fn.isoWeeks=H.fn.isoWeek,H.fn.toJSON=H.fn.toISOString,d(H.duration.fn=h.prototype,{_bubble:function(){var t,e,i,n,s=this._milliseconds,o=this._days,r=this._months,a=this._data;a.milliseconds=s%1e3,t=l(s/1e3),a.seconds=t%60,e=l(t/60),a.minutes=e%60,i=l(e/60),a.hours=i%24,o+=l(i/24),a.days=o%30,r+=l(o/30),a.months=r%12,n=l(r/12),a.years=n},weeks:function(){return l(this.days()/7)},valueOf:function(){return this._milliseconds+864e5*this._days+2592e6*(this._months%12)+31536e6*~~(this._months/12)},humanize:function(t){var e=+this,i=A(e,!t,this.lang());return t&&(i=this.lang().pastFuture(e,i)),this.lang().postformat(i)},add:function(t,e){var i=H.duration(t,e);return this._milliseconds+=i._milliseconds,this._days+=i._days,this._months+=i._months,this._bubble(),this},subtract:function(t,e){var i=H.duration(t,e);return this._milliseconds-=i._milliseconds,this._days-=i._days,this._months-=i._months,this._bubble(),this},get:function(t){return t=m(t),this[t.toLowerCase()+"s"]()},as:function(t){return t=m(t),this["as"+t.charAt(0).toUpperCase()+t.slice(1)+"s"]()},lang:H.fn.lang});for(U in le)le.hasOwnProperty(U)&&(z(U,le[U]),R(U.toLowerCase()));z("Weeks",6048e5),H.duration.fn.asMonths=function(){return(+this-31536e6*this.years())/2592e6+12*this.years()},H.lang("en",{ordinal:function(t){var e=t%10,i=1===~~(t%100/10)?"th":1===e?"st":2===e?"nd":3===e?"rd":"th";return t+i}}),B&&(i.exports=H),"undefined"==typeof ender&&(this.moment=H),"function"==typeof t&&t.amd&&t("moment",[],function(){return H})}).call(this)},{}],3:[function(e,i,n){function s(){this.subscriptions=[]}function o(t){if(this.id=A.randomUUID(),this.options=t||{},this.data={},this.fieldId=this.options.fieldId||"id",this.convert={},this.options.convert)for(var e in this.options.convert)if(this.options.convert.hasOwnProperty(e)){var i=this.options.convert[e];this.convert[e]="Date"==i||"ISODate"==i||"ASPDate"==i?"Date":i +}this.subscribers={},this.internalIds={}}function r(t,e){this.id=A.randomUUID(),this.data=null,this.ids={},this.options=e||{},this.fieldId="id",this.subscribers={};var i=this;this.listener=function(){i._onEvent.apply(i,arguments)},this.setData(t)}function a(t,e){this.parent=t,this.options=e||{},this.defaultOptions={order:function(t,e){if(t instanceof E){if(e instanceof E){var i=t.data.end-t.data.start,n=e.data.end-e.data.start;return i-n||t.data.start-e.data.start}return-1}return e instanceof E?1:t.data.start-e.data.start},margin:{item:10}},this.ordered=[]}function h(t){this.id=A.randomUUID(),this.start=null,this.end=null,this.options=t||{},this.setOptions(t)}function d(t){if("horizontal"!=t&&"vertical"!=t)throw new TypeError('Unknown direction "'+t+'". '+'Choose "horizontal" or "vertical".')}function l(t,e){return{x:t.pageX-R.util.getAbsoluteLeft(e),y:t.pageY-R.util.getAbsoluteTop(e)}}function p(){this.id=A.randomUUID(),this.components={},this.repaintTimer=void 0,this.reflowTimer=void 0}function c(){this.id=null,this.parent=null,this.depends=null,this.controller=null,this.options=null,this.frame=null,this.top=0,this.left=0,this.width=0,this.height=0}function u(t,e,i){this.id=A.randomUUID(),this.parent=t,this.depends=e,this.options=i||{}}function f(t,e){this.id=A.randomUUID(),this.container=t,this.options=e||{},this.defaultOptions={autoResize:!0},this.listeners={}}function m(t,e,i){this.id=A.randomUUID(),this.parent=t,this.depends=e,this.dom={majorLines:[],majorTexts:[],minorLines:[],minorTexts:[],redundant:{majorLines:[],majorTexts:[],minorLines:[],minorTexts:[]}},this.props={range:{start:0,end:0,minimumStep:0},lineTop:0},this.options=i||{},this.defaultOptions={orientation:"bottom",showMinorLabels:!0,showMajorLabels:!0},this.conversion=null,this.range=null}function g(t,e,i){this.id=A.randomUUID(),this.parent=t,this.depends=e,this.options=i||{},this.defaultOptions={showCurrentTime:!1}}function v(t,e,i){this.id=A.randomUUID(),this.parent=t,this.depends=e,this.options=i||{},this.defaultOptions={showCustomTime:!1},this.listeners=[],this.customTime=new Date}function y(t,e,i){this.id=A.randomUUID(),this.parent=t,this.depends=e,this.options=i||{},this.defaultOptions={type:"box",align:"center",orientation:"bottom",margin:{axis:20,item:10},padding:5},this.dom={};var n=this;this.itemsData=null,this.range=null,this.listeners={add:function(t,e,i){i!=n.id&&n._onAdd(e.items)},update:function(t,e,i){i!=n.id&&n._onUpdate(e.items)},remove:function(t,e,i){i!=n.id&&n._onRemove(e.items)}},this.items={},this.queue={},this.stack=new a(this,Object.create(this.options)),this.conversion=null}function b(t,e,i,n){this.parent=t,this.data=e,this.dom=null,this.options=i||{},this.defaultOptions=n||{},this.selected=!1,this.visible=!1,this.top=0,this.left=0,this.width=0,this.height=0}function w(t,e,i,n){this.props={dot:{left:0,top:0,width:0,height:0},line:{top:0,left:0,width:0,height:0}},b.call(this,t,e,i,n)}function _(t,e,i,n){this.props={dot:{top:0,width:0,height:0},content:{height:0,marginLeft:0}},b.call(this,t,e,i,n)}function E(t,e,i,n){this.props={content:{left:0,width:0}},b.call(this,t,e,i,n)}function T(t,e,i,n){this.props={content:{left:0,width:0}},E.call(this,t,e,i,n)}function x(t,e,i){this.id=A.randomUUID(),this.parent=t,this.groupId=e,this.itemset=null,this.options=i||{},this.options.top=0,this.props={label:{width:0,height:0}},this.top=0,this.left=0,this.width=0,this.height=0}function S(t,e,i){this.id=A.randomUUID(),this.parent=t,this.depends=e,this.options=i||{},this.range=null,this.itemsData=null,this.groupsData=null,this.groups={},this.dom={},this.props={labels:{width:0}},this.queue={};var n=this;this.listeners={add:function(t,e){n._onAdd(e.items)},update:function(t,e){n._onUpdate(e.items)},remove:function(t,e){n._onRemove(e.items)}}}function D(t,e,i){var n=this,s=I().hours(0).minutes(0).seconds(0).milliseconds(0);if(this.options={orientation:"bottom",min:null,max:null,zoomMin:10,zoomMax:31536e10,showMinorLabels:!0,showMajorLabels:!0,showCurrentTime:!1,showCustomTime:!1,autoResize:!1},this.controller=new p,!t)throw Error("No container element provided");var o=Object.create(this.options);o.height=function(){return n.options.height?n.options.height:n.timeaxis.height+n.content.height+"px"},this.rootPanel=new f(t,o),this.controller.add(this.rootPanel);var r=Object.create(this.options);r.left=function(){return n.labelPanel.width},r.width=function(){return n.rootPanel.width-n.labelPanel.width},r.top=null,r.height=null,this.itemPanel=new u(this.rootPanel,[],r),this.controller.add(this.itemPanel);var a=Object.create(this.options);a.top=null,a.left=null,a.height=null,a.width=function(){return n.content&&"function"==typeof n.content.getLabelsWidth?n.content.getLabelsWidth():0},this.labelPanel=new u(this.rootPanel,[],a),this.controller.add(this.labelPanel);var d=Object.create(this.options);this.range=new h(d),this.range.setRange(s.clone().add("days",-3).valueOf(),s.clone().add("days",4).valueOf()),this.range.subscribe(this.rootPanel,"move","horizontal"),this.range.subscribe(this.rootPanel,"zoom","horizontal"),this.range.on("rangechange",function(){var t=!0;n.controller.requestReflow(t)}),this.range.on("rangechanged",function(){var t=!0;n.controller.requestReflow(t)});var l=Object.create(o);l.range=this.range,l.left=null,l.top=null,l.width="100%",l.height=null,this.timeaxis=new m(this.itemPanel,[],l),this.timeaxis.setRange(this.range),this.controller.add(this.timeaxis),this.currenttime=new g(this.timeaxis,[],o),this.controller.add(this.currenttime),this.customtime=new v(this.timeaxis,[],o),this.controller.add(this.customtime),this.setGroups(null),this.itemsData=null,this.groupsData=null,i&&this.setOptions(i),e&&this.setItems(e)}function M(t,e,i,n){this.selected=!1,this.edges=[],this.group=n.nodes.group,this.fontSize=n.nodes.fontSize,this.fontFace=n.nodes.fontFace,this.fontColor=n.nodes.fontColor,this.color=n.nodes.color,this.id=void 0,this.shape=n.nodes.shape,this.image=n.nodes.image,this.x=0,this.y=0,this.xFixed=!1,this.yFixed=!1,this.radius=n.nodes.radius,this.radiusFixed=!1,this.radiusMin=n.nodes.radiusMin,this.radiusMax=n.nodes.radiusMax,this.imagelist=e,this.grouplist=i,this.setProperties(t,n),this.mass=50,this.fx=0,this.fy=0,this.vx=0,this.vy=0,this.minForce=n.minForce,this.damping=.9}function C(t,e,i){if(!e)throw"No graph provided";this.graph=e,this.widthMin=i.edges.widthMin,this.widthMax=i.edges.widthMax,this.id=void 0,this.fromId=void 0,this.toId=void 0,this.style=i.edges.style,this.title=void 0,this.width=i.edges.width,this.value=void 0,this.length=i.edges.length,this.from=null,this.to=null,this.connected=!1,this.dash=A.extend({},i.edges.dash),this.stiffness=void 0,this.color=i.edges.color,this.widthFixed=!1,this.lengthFixed=!1,this.setProperties(t,i)}function O(t,e,i,n){this.container=t?t:document.body,this.x=0,this.y=0,this.padding=5,void 0!==e&&void 0!==i&&this.setPosition(e,i),void 0!==n&&this.setText(n),this.frame=document.createElement("div");var s=this.frame.style;s.position="absolute",s.visibility="hidden",s.border="1px solid #666",s.color="black",s.padding=this.padding+"px",s.backgroundColor="#FFFFC6",s.borderRadius="3px",s.MozBorderRadius="3px",s.WebkitBorderRadius="3px",s.boxShadow="3px 3px 10px rgba(128, 128, 128, 0.5)",s.whiteSpace="nowrap",this.container.appendChild(this.frame)}function N(t,e,i){this.containerElement=t,this.width="100%",this.height="100%",this.refreshRate=50,this.stabilize=!0,this.selectable=!0,this.constants={nodes:{radiusMin:5,radiusMax:20,radius:5,distance:100,shape:"ellipse",image:void 0,widthMin:16,widthMax:64,fontColor:"black",fontSize:14,fontFace:"arial",color:{border:"#2B7CE9",background:"#97C2FC",highlight:{border:"#2B7CE9",background:"#D2E5FF"}},borderColor:"#2B7CE9",backgroundColor:"#97C2FC",highlightColor:"#D2E5FF",group:void 0},edges:{widthMin:1,widthMax:15,width:1,style:"line",color:"#343434",fontColor:"#343434",fontSize:14,fontFace:"arial",length:100,dash:{length:10,gap:5,altLength:void 0}},minForce:.05,minVelocity:.02,maxIterations:1e3};var n=this;this.nodes={},this.edges={},this.nodesData=null,this.edgesData=null;var s=this;this.nodesListeners={add:function(t,e){s._addNodes(e.items),s.start()},update:function(t,e){s._updateNodes(e.items),s.start()},remove:function(t,e){s._removeNodes(e.items),s.start()}},this.edgesListeners={add:function(t,e){s._addEdges(e.items),s.start()},update:function(t,e){s._updateEdges(e.items),s.start()},remove:function(t,e){s._removeEdges(e.items),s.start()}},this.groups=new Groups,this.images=new Images,this.images.setOnloadCallback(function(){n._redraw()}),this.moving=!1,this.selection=[],this.timer=void 0,this._create(),this.setOptions(i),this.setData(e)}var L,I="undefined"!=typeof window&&window.moment||e("moment");if(L="undefined"!=typeof window?window.Hammer||e("hammerjs"):function(){throw Error("hammer.js is only available in a browser, not in node.js.")},!Array.prototype.indexOf){Array.prototype.indexOf=function(t){for(var e=0;this.length>e;e++)if(this[e]==t)return e;return-1};try{console.log("Warning: Ancient browser detected. Please update your browser")}catch(k){}}Array.prototype.forEach||(Array.prototype.forEach=function(t,e){for(var i=0,n=this.length;n>i;++i)t.call(e||this,this[i],i,this)}),Array.prototype.map||(Array.prototype.map=function(t,e){var i,n,s;if(null==this)throw new TypeError(" this is null or not defined");var o=Object(this),r=o.length>>>0;if("function"!=typeof t)throw new TypeError(t+" is not a function");for(e&&(i=e),n=Array(r),s=0;r>s;){var a,h;s in o&&(a=o[s],h=t.call(i,a,s,o),n[s]=h),s++}return n}),Array.prototype.filter||(Array.prototype.filter=function(t){"use strict";if(null==this)throw new TypeError;var e=Object(this),i=e.length>>>0;if("function"!=typeof t)throw new TypeError;for(var n=[],s=arguments[1],o=0;i>o;o++)if(o in e){var r=e[o];t.call(s,r,o,e)&&n.push(r)}return n}),Object.keys||(Object.keys=function(){var t=Object.prototype.hasOwnProperty,e=!{toString:null}.propertyIsEnumerable("toString"),i=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],n=i.length;return function(s){if("object"!=typeof s&&"function"!=typeof s||null===s)throw new TypeError("Object.keys called on non-object");var o=[];for(var r in s)t.call(s,r)&&o.push(r);if(e)for(var a=0;n>a;a++)t.call(s,i[a])&&o.push(i[a]);return o}}()),Array.isArray||(Array.isArray=function(t){return"[object Array]"===Object.prototype.toString.call(t)}),Function.prototype.bind||(Function.prototype.bind=function(t){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var e=Array.prototype.slice.call(arguments,1),i=this,n=function(){},s=function(){return i.apply(this instanceof n&&t?this:t,e.concat(Array.prototype.slice.call(arguments)))};return n.prototype=this.prototype,s.prototype=new n,s}),Object.create||(Object.create=function(t){function e(){}if(arguments.length>1)throw Error("Object.create implementation only accepts the first parameter.");return e.prototype=t,new e}),Function.prototype.bind||(Function.prototype.bind=function(t){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var e=Array.prototype.slice.call(arguments,1),i=this,n=function(){},s=function(){return i.apply(this instanceof n&&t?this:t,e.concat(Array.prototype.slice.call(arguments)))};return n.prototype=this.prototype,s.prototype=new n,s});var A={};A.isNumber=function(t){return t instanceof Number||"number"==typeof t},A.isString=function(t){return t instanceof String||"string"==typeof t},A.isDate=function(t){if(t instanceof Date)return!0;if(A.isString(t)){var e=P.exec(t);if(e)return!0;if(!isNaN(Date.parse(t)))return!0}return!1},A.isDataTable=function(t){return"undefined"!=typeof google&&google.visualization&&google.visualization.DataTable&&t instanceof google.visualization.DataTable},A.randomUUID=function(){var t=function(){return Math.floor(65536*Math.random()).toString(16)};return t()+t()+"-"+t()+"-"+t()+"-"+t()+"-"+t()+t()+t()},A.extend=function(t){for(var e=1,i=arguments.length;i>e;e++){var n=arguments[e];for(var s in n)n.hasOwnProperty(s)&&void 0!==n[s]&&(t[s]=n[s])}return t},A.convert=function(t,e){var i;if(void 0===t)return void 0;if(null===t)return null;if(!e)return t;if("string"!=typeof e&&!(e instanceof String))throw Error("Type must be a string");switch(e){case"boolean":case"Boolean":return Boolean(t);case"number":case"Number":return Number(t.valueOf());case"string":case"String":return t+"";case"Date":if(A.isNumber(t))return new Date(t);if(t instanceof Date)return new Date(t.valueOf());if(I.isMoment(t))return new Date(t.valueOf());if(A.isString(t))return i=P.exec(t),i?new Date(Number(i[1])):I(t).toDate();throw Error("Cannot convert object of type "+A.getType(t)+" to type Date");case"Moment":if(A.isNumber(t))return I(t);if(t instanceof Date)return I(t.valueOf());if(I.isMoment(t))return I(t);if(A.isString(t))return i=P.exec(t),i?I(Number(i[1])):I(t);throw Error("Cannot convert object of type "+A.getType(t)+" to type Date");case"ISODate":if(A.isNumber(t))return new Date(t);if(t instanceof Date)return t.toISOString();if(I.isMoment(t))return t.toDate().toISOString();if(A.isString(t))return i=P.exec(t),i?new Date(Number(i[1])).toISOString():new Date(t).toISOString();throw Error("Cannot convert object of type "+A.getType(t)+" to type ISODate");case"ASPDate":if(A.isNumber(t))return"/Date("+t+")/";if(t instanceof Date)return"/Date("+t.valueOf()+")/";if(A.isString(t)){i=P.exec(t);var n;return n=i?new Date(Number(i[1])).valueOf():new Date(t).valueOf(),"/Date("+n+")/"}throw Error("Cannot convert object of type "+A.getType(t)+" to type ASPDate");default:throw Error("Cannot convert object of type "+A.getType(t)+' to type "'+e+'"')}};var P=/^\/?Date\((\-?\d+)/i;A.getType=function(t){var e=typeof t;return"object"==e?null==t?"null":t instanceof Boolean?"Boolean":t instanceof Number?"Number":t instanceof String?"String":t instanceof Array?"Array":t instanceof Date?"Date":"Object":"number"==e?"Number":"boolean"==e?"Boolean":"string"==e?"String":e},A.getAbsoluteLeft=function(t){for(var e=document.documentElement,i=document.body,n=t.offsetLeft,s=t.offsetParent;null!=s&&s!=i&&s!=e;)n+=s.offsetLeft,n-=s.scrollLeft,s=s.offsetParent;return n},A.getAbsoluteTop=function(t){for(var e=document.documentElement,i=document.body,n=t.offsetTop,s=t.offsetParent;null!=s&&s!=i&&s!=e;)n+=s.offsetTop,n-=s.scrollTop,s=s.offsetParent;return n},A.getPageY=function(t){if("pageY"in t)return t.pageY;var e;e="targetTouches"in t&&t.targetTouches.length?t.targetTouches[0].clientY:t.clientY;var i=document.documentElement,n=document.body;return e+(i&&i.scrollTop||n&&n.scrollTop||0)-(i&&i.clientTop||n&&n.clientTop||0)},A.getPageX=function(t){if("pageY"in t)return t.pageX;var e;e="targetTouches"in t&&t.targetTouches.length?t.targetTouches[0].clientX:t.clientX;var i=document.documentElement,n=document.body;return e+(i&&i.scrollLeft||n&&n.scrollLeft||0)-(i&&i.clientLeft||n&&n.clientLeft||0)},A.addClassName=function(t,e){var i=t.className.split(" ");-1==i.indexOf(e)&&(i.push(e),t.className=i.join(" "))},A.removeClassName=function(t,e){var i=t.className.split(" "),n=i.indexOf(e);-1!=n&&(i.splice(n,1),t.className=i.join(" "))},A.forEach=function(t,e){var i,n;if(t instanceof Array)for(i=0,n=t.length;n>i;i++)e(t[i],i,t);else for(i in t)t.hasOwnProperty(i)&&e(t[i],i,t)},A.updateProperty=function(t,e,i){return t[e]!==i?(t[e]=i,!0):!1},A.addEventListener=function(t,e,i,n){t.addEventListener?(void 0===n&&(n=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.addEventListener(e,i,n)):t.attachEvent("on"+e,i)},A.removeEventListener=function(t,e,i,n){t.removeEventListener?(void 0===n&&(n=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.removeEventListener(e,i,n)):t.detachEvent("on"+e,i)},A.getTarget=function(t){t||(t=window.event);var e;return t.target?e=t.target:t.srcElement&&(e=t.srcElement),void 0!=e.nodeType&&3==e.nodeType&&(e=e.parentNode),e},A.stopPropagation=function(t){t||(t=window.event),t.stopPropagation?t.stopPropagation():t.cancelBubble=!0},A.preventDefault=function(t){t||(t=window.event),t.preventDefault?t.preventDefault():t.returnValue=!1},A.option={},A.option.asBoolean=function(t,e){return"function"==typeof t&&(t=t()),null!=t?0!=t:e||null},A.option.asNumber=function(t,e){return"function"==typeof t&&(t=t()),null!=t?Number(t)||e||null:e||null},A.option.asString=function(t,e){return"function"==typeof t&&(t=t()),null!=t?t+"":e||null},A.option.asSize=function(t,e){return"function"==typeof t&&(t=t()),A.isString(t)?t:A.isNumber(t)?t+"px":e||null},A.option.asElement=function(t,e){return"function"==typeof t&&(t=t()),t||e||null},A.loadCss=function(t){if("undefined"!=typeof document){var e=document.createElement("style");e.type="text/css",e.styleSheet?e.styleSheet.cssText=t:e.appendChild(document.createTextNode(t)),document.getElementsByTagName("head")[0].appendChild(e)}};var F={listeners:[],indexOf:function(t){for(var e=this.listeners,i=0,n=this.listeners.length;n>i;i++){var s=e[i];if(s&&s.object==t)return i}return-1},addListener:function(t,e,i){var n=this.indexOf(t),s=this.listeners[n];s||(s={object:t,events:{}},this.listeners.push(s));var o=s.events[e];o||(o=[],s.events[e]=o),-1==o.indexOf(i)&&o.push(i)},removeListener:function(t,e,i){var n=this.indexOf(t),s=this.listeners[n];if(s){var o=s.events[e];o&&(n=o.indexOf(i),-1!=n&&o.splice(n,1),0==o.length&&delete s.events[e]);var r=0,a=s.events;for(var h in a)a.hasOwnProperty(h)&&r++;0==r&&delete this.listeners[n]}},removeAllListeners:function(){this.listeners=[]},trigger:function(t,e,i){var n=this.indexOf(t),s=this.listeners[n];if(s){var o=s.events[e];if(o)for(var r=0,a=o.length;a>r;r++)o[r](i)}}};s.prototype.on=function(t,e,i){var n=t instanceof RegExp?t:RegExp(t.replace("*","\\w+")),s={id:A.randomUUID(),event:t,regexp:n,callback:"function"==typeof e?e:null,target:i};return this.subscriptions.push(s),s.id},s.prototype.off=function(t){for(var e=0;this.subscriptions.length>e;){var i=this.subscriptions[e],n=!0;if(t instanceof Object)for(var s in t)t.hasOwnProperty(s)&&t[s]!==i[s]&&(n=!1);else n=i.id==t;n?this.subscriptions.splice(e,1):e++}},s.prototype.emit=function(t,e,i){for(var n=0;this.subscriptions.length>n;n++){var s=this.subscriptions[n];s.regexp.test(t)&&s.callback&&s.callback(t,e,i)}},o.prototype.subscribe=function(t,e){var i=this.subscribers[t];i||(i=[],this.subscribers[t]=i),i.push({callback:e})},o.prototype.unsubscribe=function(t,e){var i=this.subscribers[t];i&&(this.subscribers[t]=i.filter(function(t){return t.callback!=e}))},o.prototype._trigger=function(t,e,i){if("*"==t)throw Error("Cannot trigger event *");var n=[];t in this.subscribers&&(n=n.concat(this.subscribers[t])),"*"in this.subscribers&&(n=n.concat(this.subscribers["*"]));for(var s=0;n.length>s;s++){var o=n[s];o.callback&&o.callback(t,e,i||null)}},o.prototype.add=function(t,e){var i,n=[],s=this;if(t instanceof Array)for(var o=0,r=t.length;r>o;o++)i=s._addItem(t[o]),n.push(i);else if(A.isDataTable(t))for(var a=this._getColumnNames(t),h=0,d=t.getNumberOfRows();d>h;h++){for(var l={},p=0,c=a.length;c>p;p++){var u=a[p];l[u]=t.getValue(h,p)}i=s._addItem(l),n.push(i)}else{if(!(t instanceof Object))throw Error("Unknown dataType");i=s._addItem(t),n.push(i)}return n.length&&this._trigger("add",{items:n},e),n},o.prototype.update=function(t,e){var i=[],n=[],s=this,o=s.fieldId,r=function(t){var e=t[o];s.data[e]?(e=s._updateItem(t),n.push(e)):(e=s._addItem(t),i.push(e))};if(t instanceof Array)for(var a=0,h=t.length;h>a;a++)r(t[a]);else if(A.isDataTable(t))for(var d=this._getColumnNames(t),l=0,p=t.getNumberOfRows();p>l;l++){for(var c={},u=0,f=d.length;f>u;u++){var m=d[u];c[m]=t.getValue(l,u)}r(c)}else{if(!(t instanceof Object))throw Error("Unknown dataType");r(t)}return i.length&&this._trigger("add",{items:i},e),n.length&&this._trigger("update",{items:n},e),i.concat(n)},o.prototype.get=function(){var t,e,i,n,s=this,o=A.getType(arguments[0]);"String"==o||"Number"==o?(t=arguments[0],i=arguments[1],n=arguments[2]):"Array"==o?(e=arguments[0],i=arguments[1],n=arguments[2]):(i=arguments[0],n=arguments[1]);var r;if(i&&i.type){if(r="DataTable"==i.type?"DataTable":"Array",n&&r!=A.getType(n))throw Error('Type of parameter "data" ('+A.getType(n)+") "+"does not correspond with specified options.type ("+i.type+")");if("DataTable"==r&&!A.isDataTable(n))throw Error('Parameter "data" must be a DataTable when options.type is "DataTable"')}else r=n?"DataTable"==A.getType(n)?"DataTable":"Array":"Array";var a,h,d,l,p=i&&i.convert||this.options.convert,c=i&&i.filter,u=[];if(void 0!=t)a=s._getItem(t,p),c&&!c(a)&&(a=null);else if(void 0!=e)for(d=0,l=e.length;l>d;d++)a=s._getItem(e[d],p),(!c||c(a))&&u.push(a);else for(h in this.data)this.data.hasOwnProperty(h)&&(a=s._getItem(h,p),(!c||c(a))&&u.push(a));if(i&&i.order&&void 0==t&&this._sort(u,i.order),i&&i.fields){var f=i.fields;if(void 0!=t)a=this._filterFields(a,f);else for(d=0,l=u.length;l>d;d++)u[d]=this._filterFields(u[d],f)}if("DataTable"==r){var m=this._getColumnNames(n);if(void 0!=t)s._appendRow(n,m,a);else for(d=0,l=u.length;l>d;d++)s._appendRow(n,m,u[d]);return n}if(void 0!=t)return a;if(n){for(d=0,l=u.length;l>d;d++)n.push(u[d]);return n}return u},o.prototype.getIds=function(t){var e,i,n,s,o,r=this.data,a=t&&t.filter,h=t&&t.order,d=t&&t.convert||this.options.convert,l=[];if(a)if(h){o=[];for(n in r)r.hasOwnProperty(n)&&(s=this._getItem(n,d),a(s)&&o.push(s));for(this._sort(o,h),e=0,i=o.length;i>e;e++)l[e]=o[e][this.fieldId]}else for(n in r)r.hasOwnProperty(n)&&(s=this._getItem(n,d),a(s)&&l.push(s[this.fieldId]));else if(h){o=[];for(n in r)r.hasOwnProperty(n)&&o.push(r[n]);for(this._sort(o,h),e=0,i=o.length;i>e;e++)l[e]=o[e][this.fieldId]}else for(n in r)r.hasOwnProperty(n)&&(s=r[n],l.push(s[this.fieldId]));return l},o.prototype.forEach=function(t,e){var i,n,s=e&&e.filter,o=e&&e.convert||this.options.convert,r=this.data;if(e&&e.order)for(var a=this.get(e),h=0,d=a.length;d>h;h++)i=a[h],n=i[this.fieldId],t(i,n);else for(n in r)r.hasOwnProperty(n)&&(i=this._getItem(n,o),(!s||s(i))&&t(i,n))},o.prototype.map=function(t,e){var i,n=e&&e.filter,s=e&&e.convert||this.options.convert,o=[],r=this.data;for(var a in r)r.hasOwnProperty(a)&&(i=this._getItem(a,s),(!n||n(i))&&o.push(t(i,a)));return e&&e.order&&this._sort(o,e.order),o},o.prototype._filterFields=function(t,e){var i={};for(var n in t)t.hasOwnProperty(n)&&-1!=e.indexOf(n)&&(i[n]=t[n]);return i},o.prototype._sort=function(t,e){if(A.isString(e)){var i=e;t.sort(function(t,e){var n=t[i],s=e[i];return n>s?1:s>n?-1:0})}else{if("function"!=typeof e)throw new TypeError("Order must be a function or a string");t.sort(e)}},o.prototype.remove=function(t,e){var i,n,s,o=[];if(t instanceof Array)for(i=0,n=t.length;n>i;i++)s=this._remove(t[i]),null!=s&&o.push(s);else s=this._remove(t),null!=s&&o.push(s);return o.length&&this._trigger("remove",{items:o},e),o},o.prototype._remove=function(t){if(A.isNumber(t)||A.isString(t)){if(this.data[t])return delete this.data[t],delete this.internalIds[t],t}else if(t instanceof Object){var e=t[this.fieldId];if(e&&this.data[e])return delete this.data[e],delete this.internalIds[e],e}return null},o.prototype.clear=function(t){var e=Object.keys(this.data);return this.data={},this.internalIds={},this._trigger("remove",{items:e},t),e},o.prototype.max=function(t){var e=this.data,i=null,n=null;for(var s in e)if(e.hasOwnProperty(s)){var o=e[s],r=o[t];null!=r&&(!i||r>n)&&(i=o,n=r)}return i},o.prototype.min=function(t){var e=this.data,i=null,n=null;for(var s in e)if(e.hasOwnProperty(s)){var o=e[s],r=o[t];null!=r&&(!i||n>r)&&(i=o,n=r)}return i},o.prototype.distinct=function(t){var e=this.data,i=[],n=this.options.convert[t],s=0;for(var o in e)if(e.hasOwnProperty(o)){for(var r=e[o],a=A.convert(r[t],n),h=!1,d=0;s>d;d++)if(i[d]==a){h=!0;break}h||(i[s]=a,s++)}return i},o.prototype._addItem=function(t){var e=t[this.fieldId];if(void 0!=e){if(this.data[e])throw Error("Cannot add item: item with id "+e+" already exists")}else e=A.randomUUID(),t[this.fieldId]=e,this.internalIds[e]=t;var i={};for(var n in t)if(t.hasOwnProperty(n)){var s=this.convert[n];i[n]=A.convert(t[n],s)}return this.data[e]=i,e},o.prototype._getItem=function(t,e){var i,n,s=this.data[t];if(!s)return null;var o={},r=this.fieldId,a=this.internalIds;if(e)for(i in s)s.hasOwnProperty(i)&&(n=s[i],i==r&&n in a||(o[i]=A.convert(n,e[i])));else for(i in s)s.hasOwnProperty(i)&&(n=s[i],i==r&&n in a||(o[i]=n));return o},o.prototype._updateItem=function(t){var e=t[this.fieldId];if(void 0==e)throw Error("Cannot update item: item has no id (item: "+JSON.stringify(t)+")");var i=this.data[e];if(!i)throw Error("Cannot update item: no item with id "+e+" found");for(var n in t)if(t.hasOwnProperty(n)){var s=this.convert[n];i[n]=A.convert(t[n],s)}return e},o.prototype._getColumnNames=function(t){for(var e=[],i=0,n=t.getNumberOfColumns();n>i;i++)e[i]=t.getColumnId(i)||t.getColumnLabel(i);return e},o.prototype._appendRow=function(t,e,i){for(var n=t.addRow(),s=0,o=e.length;o>s;s++){var r=e[s];t.setValue(n,s,i[r])}},r.prototype.setData=function(t){var e,i,n;if(this.data){this.data.unsubscribe&&this.data.unsubscribe("*",this.listener),e=[];for(var s in this.ids)this.ids.hasOwnProperty(s)&&e.push(s);this.ids={},this._trigger("remove",{items:e})}if(this.data=t,this.data){for(this.fieldId=this.options.fieldId||this.data&&this.data.options&&this.data.options.fieldId||"id",e=this.data.getIds({filter:this.options&&this.options.filter}),i=0,n=e.length;n>i;i++)s=e[i],this.ids[s]=!0;this._trigger("add",{items:e}),this.data.subscribe&&this.data.subscribe("*",this.listener)}},r.prototype.get=function(){var t,e,i,n=this,s=A.getType(arguments[0]);"String"==s||"Number"==s||"Array"==s?(t=arguments[0],e=arguments[1],i=arguments[2]):(e=arguments[0],i=arguments[1]);var o=A.extend({},this.options,e);this.options.filter&&e&&e.filter&&(o.filter=function(t){return n.options.filter(t)&&e.filter(t)});var r=[];return void 0!=t&&r.push(t),r.push(o),r.push(i),this.data&&this.data.get.apply(this.data,r)},r.prototype.getIds=function(t){var e;if(this.data){var i,n=this.options.filter;i=t&&t.filter?n?function(e){return n(e)&&t.filter(e)}:t.filter:n,e=this.data.getIds({filter:i,order:t&&t.order})}else e=[];return e},r.prototype._onEvent=function(t,e,i){var n,s,o,r,a=e&&e.items,h=this.data,d=[],l=[],p=[];if(a&&h){switch(t){case"add":for(n=0,s=a.length;s>n;n++)o=a[n],r=this.get(o),r&&(this.ids[o]=!0,d.push(o));break;case"update":for(n=0,s=a.length;s>n;n++)o=a[n],r=this.get(o),r?this.ids[o]?l.push(o):(this.ids[o]=!0,d.push(o)):this.ids[o]&&(delete this.ids[o],p.push(o));break;case"remove":for(n=0,s=a.length;s>n;n++)o=a[n],this.ids[o]&&(delete this.ids[o],p.push(o))}d.length&&this._trigger("add",{items:d},i),l.length&&this._trigger("update",{items:l},i),p.length&&this._trigger("remove",{items:p},i)}},r.prototype.subscribe=o.prototype.subscribe,r.prototype.unsubscribe=o.prototype.unsubscribe,r.prototype._trigger=o.prototype._trigger,TimeStep=function(t,e,i){this.current=new Date,this._start=new Date,this._end=new Date,this.autoScale=!0,this.scale=TimeStep.SCALE.DAY,this.step=1,this.setRange(t,e,i)},TimeStep.SCALE={MILLISECOND:1,SECOND:2,MINUTE:3,HOUR:4,DAY:5,WEEKDAY:6,MONTH:7,YEAR:8},TimeStep.prototype.setRange=function(t,e,i){if(!(t instanceof Date&&e instanceof Date))throw"No legal start or end date in method setRange";this._start=void 0!=t?new Date(t.valueOf()):new Date,this._end=void 0!=e?new Date(e.valueOf()):new Date,this.autoScale&&this.setMinimumStep(i)},TimeStep.prototype.first=function(){this.current=new Date(this._start.valueOf()),this.roundToMinor()},TimeStep.prototype.roundToMinor=function(){switch(this.scale){case TimeStep.SCALE.YEAR:this.current.setFullYear(this.step*Math.floor(this.current.getFullYear()/this.step)),this.current.setMonth(0);case TimeStep.SCALE.MONTH:this.current.setDate(1);case TimeStep.SCALE.DAY:case TimeStep.SCALE.WEEKDAY:this.current.setHours(0);case TimeStep.SCALE.HOUR:this.current.setMinutes(0);case TimeStep.SCALE.MINUTE:this.current.setSeconds(0);case TimeStep.SCALE.SECOND:this.current.setMilliseconds(0)}if(1!=this.step)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current.setMilliseconds(this.current.getMilliseconds()-this.current.getMilliseconds()%this.step);break;case TimeStep.SCALE.SECOND:this.current.setSeconds(this.current.getSeconds()-this.current.getSeconds()%this.step);break;case TimeStep.SCALE.MINUTE:this.current.setMinutes(this.current.getMinutes()-this.current.getMinutes()%this.step);break;case TimeStep.SCALE.HOUR:this.current.setHours(this.current.getHours()-this.current.getHours()%this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()-1-(this.current.getDate()-1)%this.step+1);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()-this.current.getMonth()%this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()-this.current.getFullYear()%this.step);break;default:}},TimeStep.prototype.hasNext=function(){return this.current.valueOf()<=this._end.valueOf()},TimeStep.prototype.next=function(){var t=this.current.valueOf();if(6>this.current.getMonth())switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current=new Date(this.current.valueOf()+this.step);break;case TimeStep.SCALE.SECOND:this.current=new Date(this.current.valueOf()+1e3*this.step);break;case TimeStep.SCALE.MINUTE:this.current=new Date(this.current.valueOf()+60*1e3*this.step);break;case TimeStep.SCALE.HOUR:this.current=new Date(this.current.valueOf()+60*60*1e3*this.step);var e=this.current.getHours();this.current.setHours(e-e%this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()+this.step);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()+this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()+this.step);break;default:}else switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current=new Date(this.current.valueOf()+this.step);break;case TimeStep.SCALE.SECOND:this.current.setSeconds(this.current.getSeconds()+this.step);break;case TimeStep.SCALE.MINUTE:this.current.setMinutes(this.current.getMinutes()+this.step);break;case TimeStep.SCALE.HOUR:this.current.setHours(this.current.getHours()+this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()+this.step);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()+this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()+this.step);break;default:}if(1!=this.step)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current.getMilliseconds()0&&(this.step=e),this.autoScale=!1},TimeStep.prototype.setAutoScale=function(t){this.autoScale=t},TimeStep.prototype.setMinimumStep=function(t){if(void 0!=t){var e=31104e6,i=2592e6,n=864e5,s=36e5,o=6e4,r=1e3,a=1;1e3*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1e3),500*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=500),100*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=100),50*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=50),10*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=10),5*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=5),e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1),3*i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=3),i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=1),5*n>t&&(this.scale=TimeStep.SCALE.DAY,this.step=5),2*n>t&&(this.scale=TimeStep.SCALE.DAY,this.step=2),n>t&&(this.scale=TimeStep.SCALE.DAY,this.step=1),n/2>t&&(this.scale=TimeStep.SCALE.WEEKDAY,this.step=1),4*s>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=4),s>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=1),15*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=15),10*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=10),5*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=5),o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=1),15*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=15),10*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=10),5*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=5),r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=1),200*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=200),100*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=100),50*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=50),10*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=10),5*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=5),a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=1) +}},TimeStep.prototype.snap=function(t){if(this.scale==TimeStep.SCALE.YEAR){var e=t.getFullYear()+Math.round(t.getMonth()/12);t.setFullYear(Math.round(e/this.step)*this.step),t.setMonth(0),t.setDate(0),t.setHours(0),t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.MONTH)t.getDate()>15?(t.setDate(1),t.setMonth(t.getMonth()+1)):t.setDate(1),t.setHours(0),t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0);else if(this.scale==TimeStep.SCALE.DAY||this.scale==TimeStep.SCALE.WEEKDAY){switch(this.step){case 5:case 2:t.setHours(24*Math.round(t.getHours()/24));break;default:t.setHours(12*Math.round(t.getHours()/12))}t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.HOUR){switch(this.step){case 4:t.setMinutes(60*Math.round(t.getMinutes()/60));break;default:t.setMinutes(30*Math.round(t.getMinutes()/30))}t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.MINUTE){switch(this.step){case 15:case 10:t.setMinutes(5*Math.round(t.getMinutes()/5)),t.setSeconds(0);break;case 5:t.setSeconds(60*Math.round(t.getSeconds()/60));break;default:t.setSeconds(30*Math.round(t.getSeconds()/30))}t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.SECOND)switch(this.step){case 15:case 10:t.setSeconds(5*Math.round(t.getSeconds()/5)),t.setMilliseconds(0);break;case 5:t.setMilliseconds(1e3*Math.round(t.getMilliseconds()/1e3));break;default:t.setMilliseconds(500*Math.round(t.getMilliseconds()/500))}else if(this.scale==TimeStep.SCALE.MILLISECOND){var i=this.step>5?this.step/2:1;t.setMilliseconds(Math.round(t.getMilliseconds()/i)*i)}},TimeStep.prototype.isMajor=function(){switch(this.scale){case TimeStep.SCALE.MILLISECOND:return 0==this.current.getMilliseconds();case TimeStep.SCALE.SECOND:return 0==this.current.getSeconds();case TimeStep.SCALE.MINUTE:return 0==this.current.getHours()&&0==this.current.getMinutes();case TimeStep.SCALE.HOUR:return 0==this.current.getHours();case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:return 1==this.current.getDate();case TimeStep.SCALE.MONTH:return 0==this.current.getMonth();case TimeStep.SCALE.YEAR:return!1;default:return!1}},TimeStep.prototype.getLabelMinor=function(t){switch(void 0==t&&(t=this.current),this.scale){case TimeStep.SCALE.MILLISECOND:return I(t).format("SSS");case TimeStep.SCALE.SECOND:return I(t).format("s");case TimeStep.SCALE.MINUTE:return I(t).format("HH:mm");case TimeStep.SCALE.HOUR:return I(t).format("HH:mm");case TimeStep.SCALE.WEEKDAY:return I(t).format("ddd D");case TimeStep.SCALE.DAY:return I(t).format("D");case TimeStep.SCALE.MONTH:return I(t).format("MMM");case TimeStep.SCALE.YEAR:return I(t).format("YYYY");default:return""}},TimeStep.prototype.getLabelMajor=function(t){switch(void 0==t&&(t=this.current),this.scale){case TimeStep.SCALE.MILLISECOND:return I(t).format("HH:mm:ss");case TimeStep.SCALE.SECOND:return I(t).format("D MMMM HH:mm");case TimeStep.SCALE.MINUTE:case TimeStep.SCALE.HOUR:return I(t).format("ddd D MMMM");case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:return I(t).format("MMMM YYYY");case TimeStep.SCALE.MONTH:return I(t).format("YYYY");case TimeStep.SCALE.YEAR:return"";default:return""}},a.prototype.setOptions=function(t){A.extend(this.options,t)},a.prototype.update=function(){this._order(),this._stack()},a.prototype._order=function(){var t=this.parent.items;if(!t)throw Error("Cannot stack items: parent does not contain items");var e=[],i=0;A.forEach(t,function(t){t.visible&&(e[i]=t,i++)});var n=this.options.order||this.defaultOptions.order;if("function"!=typeof n)throw Error("Option order must be a function");e.sort(n),this.ordered=e},a.prototype._stack=function(){var t,e,i,n=this.ordered,s=this.options,o=s.orientation||this.defaultOptions.orientation,r="top"==o;for(i=s.margin&&void 0!==s.margin.item?s.margin.item:this.defaultOptions.margin.item,t=0,e=n.length;e>t;t++){var a=n[t],h=null;do h=this.checkOverlap(n,t,0,t-1,i),null!=h&&(a.top=r?h.top+h.height+i:h.top-a.height-i);while(h)}},a.prototype.checkOverlap=function(t,e,i,n,s){for(var o=this.collision,r=t[e],a=n;a>=i;a--){var h=t[a];if(o(r,h,s)&&a!=e)return h}return null},a.prototype.collision=function(t,e,i){return t.left-ie.left&&t.top-ie.top},h.prototype.setOptions=function(t){A.extend(this.options,t),null!==this.start&&null!==this.end&&this.setRange(this.start,this.end)},h.prototype.subscribe=function(t,e,i){function n(e){s._onMouseWheel(e,t,i)}var s=this;if("move"==e)t.on("dragstart",function(e){s._onDragStart(e,t)}),t.on("drag",function(e){s._onDrag(e,t,i)}),t.on("dragend",function(e){s._onDragEnd(e,t)});else{if("zoom"!=e)throw new TypeError('Unknown event "'+e+'". '+'Choose "move" or "zoom".');t.on("mousewheel",n),t.on("DOMMouseScroll",n),t.on("touch",function(){s._onTouch()}),t.on("pinch",function(e){s._onPinch(e,t,i)})}},h.prototype.on=function(t,e){F.addListener(this,t,e)},h.prototype._trigger=function(t){F.trigger(this,t,{start:this.start,end:this.end})},h.prototype.setRange=function(t,e){var i=this._applyRange(t,e);i&&(this._trigger("rangechange"),this._trigger("rangechanged"))},h.prototype._applyRange=function(t,e){var i,n=null!=t?A.convert(t,"Number"):this.start,s=null!=e?A.convert(e,"Number"):this.end,o=null!=this.options.max?A.convert(this.options.max,"Date").valueOf():null,r=null!=this.options.min?A.convert(this.options.min,"Date").valueOf():null;if(isNaN(n)||null===n)throw Error('Invalid start "'+t+'"');if(isNaN(s)||null===s)throw Error('Invalid end "'+e+'"');if(n>s&&(s=n),null!==r&&r>n&&(i=r-n,n+=i,s+=i,null!=o&&s>o&&(s=o)),null!==o&&s>o&&(i=s-o,n-=i,s-=i,null!=r&&r>n&&(n=r)),null!==this.options.zoomMin){var a=parseFloat(this.options.zoomMin);0>a&&(a=0),a>s-n&&(this.end-this.start===a?(n=this.start,s=this.end):(i=a-(s-n),n-=i/2,s+=i/2))}if(null!==this.options.zoomMax){var h=parseFloat(this.options.zoomMax);0>h&&(h=0),s-n>h&&(this.end-this.start===h?(n=this.start,s=this.end):(i=s-n-h,n+=i/2,s-=i/2))}var d=this.start!=n||this.end!=s;return this.start=n,this.end=s,d},h.prototype.getRange=function(){return{start:this.start,end:this.end}},h.prototype.conversion=function(t){return h.conversion(this.start,this.end,t)},h.conversion=function(t,e,i){return 0!=i&&0!=e-t?{offset:t,scale:i/(e-t)}:{offset:0,scale:1}};var Y={};h.prototype._onDragStart=function(t,e){if(!Y.pinching){Y.start=this.start,Y.end=this.end;var i=e.frame;i&&(i.style.cursor="move")}},h.prototype._onDrag=function(t,e,i){if(d(i),!Y.pinching){var n="horizontal"==i?t.gesture.deltaX:t.gesture.deltaY,s=Y.end-Y.start,o="horizontal"==i?e.width:e.height,r=-n/o*s;this._applyRange(Y.start+r,Y.end+r),this._trigger("rangechange")}},h.prototype._onDragEnd=function(t,e){Y.pinching||(e.frame&&(e.frame.style.cursor="auto"),this._trigger("rangechanged"))},h.prototype._onMouseWheel=function(t,e,i){d(i);var n=0;if(t.wheelDelta?n=t.wheelDelta/120:t.detail&&(n=-t.detail/3),n){var s;s=0>n?1-n/5:1/(1+n/5);var o=L.event.collectEventData(this,"scroll",t),r=l(o.touches[0],e.frame),a=this._pointerToDate(e,i,r);this.zoom(s,a)}A.preventDefault(t)},h.prototype._onTouch=function(){Y.start=this.start,Y.end=this.end,Y.pinching=!1,Y.center=null},h.prototype._onPinch=function(t,e,i){if(Y.pinching=!0,t.gesture.touches.length>1){Y.center||(Y.center=l(t.gesture.center,e.frame));var n=1/t.gesture.scale,s=this._pointerToDate(e,i,Y.center),o=l(t.gesture.center,e.frame);this._pointerToDate(e,i,o);var r=parseInt(s+(Y.start-s)*n),a=parseInt(s+(Y.end-s)*n);this.setRange(r,a)}},h.prototype._pointerToDate=function(t,e,i){var n;if("horizontal"==e){var s=t.width;return n=this.conversion(s),i.x/n.scale+n.offset}var o=t.height;return n=this.conversion(o),i.y/n.scale+n.offset},h.prototype.zoom=function(t,e){null==e&&(e=(this.start+this.end)/2);var i=e+(this.start-e)*t,n=e+(this.end-e)*t;this.setRange(i,n)},h.prototype.move=function(t){var e=this.end-this.start,i=this.start+e*t,n=this.end+e*t;this.start=i,this.end=n},h.prototype.moveTo=function(t){var e=(this.start+this.end)/2,i=e-t,n=this.start-i,s=this.end-i;this.setRange(n,s)},p.prototype.add=function(t){if(void 0==t.id)throw Error("Component has no field id");if(!(t instanceof c||t instanceof p))throw new TypeError("Component must be an instance of prototype Component or Controller");t.controller=this,this.components[t.id]=t},p.prototype.remove=function(t){var e;for(e in this.components)if(this.components.hasOwnProperty(e)&&(e==t||this.components[e]==t))break;e&&delete this.components[e]},p.prototype.requestReflow=function(t){if(t)this.reflow();else if(!this.reflowTimer){var e=this;this.reflowTimer=setTimeout(function(){e.reflowTimer=void 0,e.reflow()},0)}},p.prototype.requestRepaint=function(t){if(t)this.repaint();else if(!this.repaintTimer){var e=this;this.repaintTimer=setTimeout(function(){e.repaintTimer=void 0,e.repaint()},0)}},p.prototype.repaint=function(){function t(n,s){s in i||(n.depends&&n.depends.forEach(function(e){t(e,e.id)}),n.parent&&t(n.parent,n.parent.id),e=n.repaint()||e,i[s]=!0)}var e=!1;this.repaintTimer&&(clearTimeout(this.repaintTimer),this.repaintTimer=void 0);var i={};A.forEach(this.components,t),e&&this.reflow()},p.prototype.reflow=function(){function t(n,s){s in i||(n.depends&&n.depends.forEach(function(e){t(e,e.id)}),n.parent&&t(n.parent,n.parent.id),e=n.reflow()||e,i[s]=!0)}var e=!1;this.reflowTimer&&(clearTimeout(this.reflowTimer),this.reflowTimer=void 0);var i={};A.forEach(this.components,t),e&&this.repaint()},c.prototype.setOptions=function(t){t&&(A.extend(this.options,t),this.controller&&(this.requestRepaint(),this.requestReflow()))},c.prototype.getOption=function(t){var e;return this.options&&(e=this.options[t]),void 0===e&&this.defaultOptions&&(e=this.defaultOptions[t]),e},c.prototype.getContainer=function(){return null},c.prototype.getFrame=function(){return this.frame},c.prototype.repaint=function(){return!1},c.prototype.reflow=function(){return!1},c.prototype.hide=function(){return this.frame&&this.frame.parentNode?(this.frame.parentNode.removeChild(this.frame),!0):!1},c.prototype.show=function(){return this.frame&&this.frame.parentNode?!1:this.repaint()},c.prototype.requestRepaint=function(){if(!this.controller)throw Error("Cannot request a repaint: no controller configured");this.controller.requestRepaint()},c.prototype.requestReflow=function(){if(!this.controller)throw Error("Cannot request a reflow: no controller configured");this.controller.requestReflow()},u.prototype=new c,u.prototype.setOptions=c.prototype.setOptions,u.prototype.getContainer=function(){return this.frame},u.prototype.repaint=function(){var t=0,e=A.updateProperty,i=A.option.asSize,n=this.options,s=this.frame;if(!s){s=document.createElement("div"),s.className="panel";var o=n.className;o&&("function"==typeof o?A.addClassName(s,o()+""):A.addClassName(s,o+"")),this.frame=s,t+=1}if(!s.parentNode){if(!this.parent)throw Error("Cannot repaint panel: no parent attached");var r=this.parent.getContainer();if(!r)throw Error("Cannot repaint panel: parent has no container element");r.appendChild(s),t+=1}return t+=e(s.style,"top",i(n.top,"0px")),t+=e(s.style,"left",i(n.left,"0px")),t+=e(s.style,"width",i(n.width,"100%")),t+=e(s.style,"height",i(n.height,"100%")),t>0},u.prototype.reflow=function(){var t=0,e=A.updateProperty,i=this.frame;return i?(t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft),t+=e(this,"width",i.offsetWidth),t+=e(this,"height",i.offsetHeight)):t+=1,t>0},f.prototype=new u,f.prototype.setOptions=c.prototype.setOptions,f.prototype.repaint=function(){var t=0,e=A.updateProperty,i=A.option.asSize,n=this.options,s=this.frame;if(s||(s=document.createElement("div"),this.frame=s,t+=1),!s.parentNode){if(!this.container)throw Error("Cannot repaint root panel: no container attached");this.container.appendChild(s),t+=1}s.className="vis timeline rootpanel "+n.orientation;var o=n.className;return o&&A.addClassName(s,A.option.asString(o)),t+=e(s.style,"top",i(n.top,"0px")),t+=e(s.style,"left",i(n.left,"0px")),t+=e(s.style,"width",i(n.width,"100%")),t+=e(s.style,"height",i(n.height,"100%")),this._updateEventEmitters(),this._updateWatch(),t>0},f.prototype.reflow=function(){var t=0,e=A.updateProperty,i=this.frame;return i?(t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft),t+=e(this,"width",i.offsetWidth),t+=e(this,"height",i.offsetHeight)):t+=1,t>0},f.prototype._updateWatch=function(){var t=this.getOption("autoResize");t?this._watch():this._unwatch()},f.prototype._watch=function(){var t=this;this._unwatch();var e=function(){var e=t.getOption("autoResize");return e?(t.frame&&(t.frame.clientWidth!=t.width||t.frame.clientHeight!=t.height)&&t.requestReflow(),void 0):(t._unwatch(),void 0)};A.addEventListener(window,"resize",e),this.watchTimer=setInterval(e,1e3)},f.prototype._unwatch=function(){this.watchTimer&&(clearInterval(this.watchTimer),this.watchTimer=void 0)},f.prototype.on=function(t,e){var i=this.listeners[t];i||(i=[],this.listeners[t]=i),i.push(e),this._updateEventEmitters()},f.prototype._updateEventEmitters=function(){if(this.listeners){var t=this;A.forEach(this.listeners,function(e,i){if(t.emitters||(t.emitters={}),!(i in t.emitters)){var n=t.frame;if(n){var s=function(t){e.forEach(function(e){e(t)})};t.emitters[i]=s,t.hammer||(t.hammer=L(n,{prevent_default:!0})),t.hammer.on(i,s)}}})}},m.prototype=new c,m.prototype.setOptions=c.prototype.setOptions,m.prototype.setRange=function(t){if(!(t instanceof h||t&&t.start&&t.end))throw new TypeError("Range must be an instance of Range, or an object containing start and end.");this.range=t},m.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.scale+e.offset)},m.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.scale},m.prototype.repaint=function(){var t=0,e=A.updateProperty,i=A.option.asSize,n=this.options,s=this.getOption("orientation"),o=this.props,r=this.step,a=this.frame;if(a||(a=document.createElement("div"),this.frame=a,t+=1),a.className="axis",!a.parentNode){if(!this.parent)throw Error("Cannot repaint time axis: no parent attached");var h=this.parent.getContainer();if(!h)throw Error("Cannot repaint time axis: parent has no container element");h.appendChild(a),t+=1}var d=a.parentNode;if(d){var l=a.nextSibling;d.removeChild(a);var p="bottom"==s&&this.props.parentHeight&&this.height?this.props.parentHeight-this.height+"px":"0px";if(t+=e(a.style,"top",i(n.top,p)),t+=e(a.style,"left",i(n.left,"0px")),t+=e(a.style,"width",i(n.width,"100%")),t+=e(a.style,"height",i(n.height,this.height+"px")),this._repaintMeasureChars(),this.step){this._repaintStart(),r.first();for(var c=void 0,u=0;r.hasNext()&&1e3>u;){u++;var f=r.getCurrent(),m=this.toScreen(f),g=r.isMajor();this.getOption("showMinorLabels")&&this._repaintMinorText(m,r.getLabelMinor()),g&&this.getOption("showMajorLabels")?(m>0&&(void 0==c&&(c=m),this._repaintMajorText(m,r.getLabelMajor())),this._repaintMajorLine(m)):this._repaintMinorLine(m),r.next()}if(this.getOption("showMajorLabels")){var v=this.toTime(0),y=r.getLabelMajor(v),b=y.length*(o.majorCharWidth||10)+10;(void 0==c||c>b)&&this._repaintMajorText(0,y)}this._repaintEnd()}this._repaintLine(),l?d.insertBefore(a,l):d.appendChild(a)}return t>0},m.prototype._repaintStart=function(){var t=this.dom,e=t.redundant;e.majorLines=t.majorLines,e.majorTexts=t.majorTexts,e.minorLines=t.minorLines,e.minorTexts=t.minorTexts,t.majorLines=[],t.majorTexts=[],t.minorLines=[],t.minorTexts=[]},m.prototype._repaintEnd=function(){A.forEach(this.dom.redundant,function(t){for(;t.length;){var e=t.pop();e&&e.parentNode&&e.parentNode.removeChild(e)}})},m.prototype._repaintMinorText=function(t,e){var i=this.dom.redundant.minorTexts.shift();if(!i){var n=document.createTextNode("");i=document.createElement("div"),i.appendChild(n),i.className="text minor",this.frame.appendChild(i)}this.dom.minorTexts.push(i),i.childNodes[0].nodeValue=e,i.style.left=t+"px",i.style.top=this.props.minorLabelTop+"px"},m.prototype._repaintMajorText=function(t,e){var i=this.dom.redundant.majorTexts.shift();if(!i){var n=document.createTextNode(e);i=document.createElement("div"),i.className="text major",i.appendChild(n),this.frame.appendChild(i)}this.dom.majorTexts.push(i),i.childNodes[0].nodeValue=e,i.style.top=this.props.majorLabelTop+"px",i.style.left=t+"px"},m.prototype._repaintMinorLine=function(t){var e=this.dom.redundant.minorLines.shift();e||(e=document.createElement("div"),e.className="grid vertical minor",this.frame.appendChild(e)),this.dom.minorLines.push(e);var i=this.props;e.style.top=i.minorLineTop+"px",e.style.height=i.minorLineHeight+"px",e.style.left=t-i.minorLineWidth/2+"px"},m.prototype._repaintMajorLine=function(t){var e=this.dom.redundant.majorLines.shift();e||(e=document.createElement("DIV"),e.className="grid vertical major",this.frame.appendChild(e)),this.dom.majorLines.push(e);var i=this.props;e.style.top=i.majorLineTop+"px",e.style.left=t-i.majorLineWidth/2+"px",e.style.height=i.majorLineHeight+"px"},m.prototype._repaintLine=function(){var t=this.dom.line,e=this.frame;this.options,this.getOption("showMinorLabels")||this.getOption("showMajorLabels")?(t?(e.removeChild(t),e.appendChild(t)):(t=document.createElement("div"),t.className="grid horizontal major",e.appendChild(t),this.dom.line=t),t.style.top=this.props.lineTop+"px"):t&&t.parentElement&&(e.removeChild(t.line),delete this.dom.line)},m.prototype._repaintMeasureChars=function(){var t,e=this.dom;if(!e.measureCharMinor){t=document.createTextNode("0");var i=document.createElement("DIV");i.className="text minor measure",i.appendChild(t),this.frame.appendChild(i),e.measureCharMinor=i}if(!e.measureCharMajor){t=document.createTextNode("0");var n=document.createElement("DIV");n.className="text major measure",n.appendChild(t),this.frame.appendChild(n),e.measureCharMajor=n}},m.prototype.reflow=function(){var t=0,e=A.updateProperty,i=this.frame,n=this.range;if(!n)throw Error("Cannot repaint time axis: no range configured");if(i){t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft);var s=this.props,o=this.getOption("showMinorLabels"),r=this.getOption("showMajorLabels"),a=this.dom.measureCharMinor,h=this.dom.measureCharMajor;a&&(s.minorCharHeight=a.clientHeight,s.minorCharWidth=a.clientWidth),h&&(s.majorCharHeight=h.clientHeight,s.majorCharWidth=h.clientWidth);var d=i.parentNode?i.parentNode.offsetHeight:0;switch(d!=s.parentHeight&&(s.parentHeight=d,t+=1),this.getOption("orientation")){case"bottom":s.minorLabelHeight=o?s.minorCharHeight:0,s.majorLabelHeight=r?s.majorCharHeight:0,s.minorLabelTop=0,s.majorLabelTop=s.minorLabelTop+s.minorLabelHeight,s.minorLineTop=-this.top,s.minorLineHeight=Math.max(this.top+s.majorLabelHeight,0),s.minorLineWidth=1,s.majorLineTop=-this.top,s.majorLineHeight=Math.max(this.top+s.minorLabelHeight+s.majorLabelHeight,0),s.majorLineWidth=1,s.lineTop=0;break;case"top":s.minorLabelHeight=o?s.minorCharHeight:0,s.majorLabelHeight=r?s.majorCharHeight:0,s.majorLabelTop=0,s.minorLabelTop=s.majorLabelTop+s.majorLabelHeight,s.minorLineTop=s.minorLabelTop,s.minorLineHeight=Math.max(d-s.majorLabelHeight-this.top),s.minorLineWidth=1,s.majorLineTop=0,s.majorLineHeight=Math.max(d-this.top),s.majorLineWidth=1,s.lineTop=s.majorLabelHeight+s.minorLabelHeight;break;default:throw Error('Unkown orientation "'+this.getOption("orientation")+'"')}var l=s.minorLabelHeight+s.majorLabelHeight;t+=e(this,"width",i.offsetWidth),t+=e(this,"height",l),this._updateConversion();var p=A.convert(n.start,"Number"),c=A.convert(n.end,"Number"),u=this.toTime(5*(s.minorCharWidth||10)).valueOf()-this.toTime(0).valueOf();this.step=new TimeStep(new Date(p),new Date(c),u),t+=e(s.range,"start",p),t+=e(s.range,"end",c),t+=e(s.range,"minimumStep",u.valueOf())}return t>0},m.prototype._updateConversion=function(){var t=this.range;if(!t)throw Error("No range configured");this.conversion=t.conversion?t.conversion(this.width):h.conversion(t.start,t.end,this.width)},g.prototype=new c,g.prototype.setOptions=c.prototype.setOptions,g.prototype.getContainer=function(){return this.frame},g.prototype.repaint=function(){var t=this.frame,e=this.parent,i=e.parent.getContainer();if(!e)throw Error("Cannot repaint bar: no parent attached");if(!i)throw Error("Cannot repaint bar: parent has no container element");if(!this.getOption("showCurrentTime"))return t&&(i.removeChild(t),delete this.frame),void 0;t||(t=document.createElement("div"),t.className="currenttime",t.style.position="absolute",t.style.top="0px",t.style.height="100%",i.appendChild(t),this.frame=t),e.conversion||e._updateConversion();var n=new Date,s=e.toScreen(n);t.style.left=s+"px",t.title="Current time: "+n,void 0!==this.currentTimeTimer&&(clearTimeout(this.currentTimeTimer),delete this.currentTimeTimer);var o=this,r=1/e.conversion.scale/2;return 30>r&&(r=30),this.currentTimeTimer=setTimeout(function(){o.repaint()},r),!1},v.prototype=new c,v.prototype.setOptions=c.prototype.setOptions,v.prototype.getContainer=function(){return this.frame},v.prototype.repaint=function(){var t=this.frame,e=this.parent,i=e.parent.getContainer();if(!e)throw Error("Cannot repaint bar: no parent attached");if(!i)throw Error("Cannot repaint bar: parent has no container element");if(!this.getOption("showCustomTime"))return t&&(i.removeChild(t),delete this.frame),void 0;if(!t){t=document.createElement("div"),t.className="customtime",t.style.position="absolute",t.style.top="0px",t.style.height="100%",i.appendChild(t);var n=document.createElement("div");n.style.position="relative",n.style.top="0px",n.style.left="-10px",n.style.height="100%",n.style.width="20px",t.appendChild(n),this.frame=t,this.subscribe(this,"movetime")}e.conversion||e._updateConversion();var s=e.toScreen(this.customTime);return t.style.left=s+"px",t.title="Time: "+this.customTime,!1},v.prototype._setCustomTime=function(t){this.customTime=new Date(t.valueOf()),this.repaint()},v.prototype._getCustomTime=function(){return new Date(this.customTime.valueOf())},v.prototype.subscribe=function(t,e){var i=this,n={component:t,event:e,callback:function(t){i._onMouseDown(t,n)},params:{}};t.on("mousedown",n.callback),i.listeners.push(n)},v.prototype.on=function(t,e){var i=this.frame;if(!i)throw Error("Cannot add event listener: no parent attached");F.addListener(this,t,e),A.addEventListener(i,t,e)},v.prototype._onMouseDown=function(t,e){t=t||window.event;var i=e.params,n=t.which?1==t.which:1==t.button;if(n){i.mouseX=A.getPageX(t),i.moved=!1,i.customTime=this.customTime;var s=this;i.onMouseMove||(i.onMouseMove=function(t){s._onMouseMove(t,e)},A.addEventListener(document,"mousemove",i.onMouseMove)),i.onMouseUp||(i.onMouseUp=function(t){s._onMouseUp(t,e)},A.addEventListener(document,"mouseup",i.onMouseUp)),A.stopPropagation(t),A.preventDefault(t)}},v.prototype._onMouseMove=function(t,e){t=t||window.event;var i=e.params,n=this.parent,s=A.getPageX(t);void 0===i.mouseX&&(i.mouseX=s);var o=s-i.mouseX;Math.abs(o)>=1&&(i.moved=!0);var r=n.toScreen(i.customTime),a=r+o,h=n.toTime(a);this._setCustomTime(h),F.trigger(this,"timechange",{customTime:this.customTime}),A.preventDefault(t)},v.prototype._onMouseUp=function(t,e){t=t||window.event;var i=e.params;i.onMouseMove&&(A.removeEventListener(document,"mousemove",i.onMouseMove),i.onMouseMove=null),i.onMouseUp&&(A.removeEventListener(document,"mouseup",i.onMouseUp),i.onMouseUp=null),i.moved&&F.trigger(this,"timechanged",{customTime:this.customTime})},y.prototype=new u,y.types={box:w,range:E,rangeoverflow:T,point:_},y.prototype.setOptions=c.prototype.setOptions,y.prototype.setRange=function(t){if(!(t instanceof h||t&&t.start&&t.end))throw new TypeError("Range must be an instance of Range, or an object containing start and end.");this.range=t},y.prototype.repaint=function(){var t=0,e=A.updateProperty,i=A.option.asSize,n=this.options,s=this.getOption("orientation"),o=this.defaultOptions,r=this.frame;if(!r){r=document.createElement("div"),r.className="itemset";var a=n.className;a&&A.addClassName(r,A.option.asString(a));var h=document.createElement("div");h.className="background",r.appendChild(h),this.dom.background=h;var d=document.createElement("div");d.className="foreground",r.appendChild(d),this.dom.foreground=d;var l=document.createElement("div");l.className="itemset-axis",this.dom.axis=l,this.frame=r,t+=1}if(!this.parent)throw Error("Cannot repaint itemset: no parent attached");var p=this.parent.getContainer();if(!p)throw Error("Cannot repaint itemset: parent has no container element");r.parentNode||(p.appendChild(r),t+=1),this.dom.axis.parentNode||(p.appendChild(this.dom.axis),t+=1),t+=e(r.style,"left",i(n.left,"0px")),t+=e(r.style,"top",i(n.top,"0px")),t+=e(r.style,"width",i(n.width,"100%")),t+=e(r.style,"height",i(n.height,this.height+"px")),t+=e(this.dom.axis.style,"left",i(n.left,"0px")),t+=e(this.dom.axis.style,"width",i(n.width,"100%")),t+="bottom"==s?e(this.dom.axis.style,"top",this.height+this.top+"px"):e(this.dom.axis.style,"top",this.top+"px"),this._updateConversion();var c=this,u=this.queue,f=this.itemsData,m=this.items,g={};return Object.keys(u).forEach(function(e){var i=u[e],s=m[e];switch(i){case"add":case"update":var r=f&&f.get(e,g);if(r){var a=r.type||r.start&&r.end&&"range"||n.type||"box",h=y.types[a];if(s&&(h&&s instanceof h?(s.data=r,t++):(t+=s.hide(),s=null)),!s){if(!h)throw new TypeError('Unknown item type "'+a+'"');s=new h(c,r,n,o),t++}s.repaint(),m[e]=s}delete u[e];break;case"remove":s&&(t+=s.hide()),delete m[e],delete u[e];break;default:console.log('Error: unknown action "'+i+'"')}}),A.forEach(this.items,function(e){e.visible?(t+=e.show(),e.reposition()):t+=e.hide()}),t>0},y.prototype.getForeground=function(){return this.dom.foreground},y.prototype.getBackground=function(){return this.dom.background},y.prototype.getAxis=function(){return this.dom.axis},y.prototype.reflow=function(){var t=0,e=this.options,i=e.margin&&e.margin.axis||this.defaultOptions.margin.axis,n=e.margin&&e.margin.item||this.defaultOptions.margin.item,s=A.updateProperty,o=A.option.asNumber,r=A.option.asSize,a=this.frame;if(a){this._updateConversion(),A.forEach(this.items,function(e){t+=e.reflow()}),this.stack.update();var h,d=o(e.maxHeight),l=null!=r(e.height);if(l)h=a.offsetHeight;else{var p=this.stack.ordered;if(p.length){var c=p[0].top,u=p[0].top+p[0].height;A.forEach(p,function(t){c=Math.min(c,t.top),u=Math.max(u,t.top+t.height)}),h=u-c+i+n}else h=i+n}null!=d&&(h=Math.min(h,d)),t+=s(this,"height",h),t+=s(this,"top",a.offsetTop),t+=s(this,"left",a.offsetLeft),t+=s(this,"width",a.offsetWidth)}else t+=1;return t>0},y.prototype.hide=function(){var t=!1;return this.frame&&this.frame.parentNode&&(this.frame.parentNode.removeChild(this.frame),t=!0),this.dom.axis&&this.dom.axis.parentNode&&(this.dom.axis.parentNode.removeChild(this.dom.axis),t=!0),t},y.prototype.setItems=function(t){var e,i=this,n=this.itemsData;if(t){if(!(t instanceof o||t instanceof r))throw new TypeError("Data must be an instance of DataSet");this.itemsData=t}else this.itemsData=null;if(n&&(A.forEach(this.listeners,function(t,e){n.unsubscribe(e,t)}),e=n.getIds(),this._onRemove(e)),this.itemsData){var s=this.id;A.forEach(this.listeners,function(t,e){i.itemsData.subscribe(e,t,s)}),e=this.itemsData.getIds(),this._onAdd(e)}},y.prototype.getItems=function(){return this.itemsData},y.prototype._onUpdate=function(t){this._toQueue("update",t)},y.prototype._onAdd=function(t){this._toQueue("add",t)},y.prototype._onRemove=function(t){this._toQueue("remove",t)},y.prototype._toQueue=function(t,e){var i=this.queue;e.forEach(function(e){i[e]=t}),this.controller&&this.requestRepaint()},y.prototype._updateConversion=function(){var t=this.range;if(!t)throw Error("No range configured");this.conversion=t.conversion?t.conversion(this.width):h.conversion(t.start,t.end,this.width)},y.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.scale+e.offset)},y.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.scale},b.prototype.select=function(){this.selected=!0},b.prototype.unselect=function(){this.selected=!1},b.prototype.show=function(){return!1},b.prototype.hide=function(){return!1},b.prototype.repaint=function(){return!1},b.prototype.reflow=function(){return!1},b.prototype.getWidth=function(){return this.width},w.prototype=new b(null,null),w.prototype.select=function(){this.selected=!0},w.prototype.unselect=function(){this.selected=!1},w.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw Error("Cannot repaint item: no parent attached");if(!e.box.parentNode){var i=this.parent.getForeground();if(!i)throw Error("Cannot repaint time axis: parent has no foreground container element");i.appendChild(e.box),t=!0}if(!e.line.parentNode){var n=this.parent.getBackground();if(!n)throw Error("Cannot repaint time axis: parent has no background container element");n.appendChild(e.line),t=!0}if(!e.dot.parentNode){var s=this.parent.getAxis();if(!n)throw Error("Cannot repaint time axis: parent has no axis container element");s.appendChild(e.dot),t=!0}if(this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var o=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=o&&(this.className=o,e.box.className="item box"+o,e.line.className="item line"+o,e.dot.className="item dot"+o,t=!0)}return t},w.prototype.show=function(){return this.dom&&this.dom.box.parentNode?!1:this.repaint()},w.prototype.hide=function(){var t=!1,e=this.dom;return e&&(e.box.parentNode&&(e.box.parentNode.removeChild(e.box),t=!0),e.line.parentNode&&e.line.parentNode.removeChild(e.line),e.dot.parentNode&&e.dot.parentNode.removeChild(e.dot)),t},w.prototype.reflow=function(){var t,e,i,n,s,o,r,a,h,d,l,p,c=0;if(void 0==this.data.start)throw Error('Property "start" missing in item '+this.data.id);if(l=this.data,p=this.parent&&this.parent.range,l&&p){var u=p.end-p.start;this.visible=l.start>p.start-u&&l.start0},w.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.box=document.createElement("DIV"),t.content=document.createElement("DIV"),t.content.className="content",t.box.appendChild(t.content),t.line=document.createElement("DIV"),t.line.className="line",t.dot=document.createElement("DIV"),t.dot.className="dot")},w.prototype.reposition=function(){var t=this.dom,e=this.props,i=this.options.orientation||this.defaultOptions.orientation;if(t){var n=t.box,s=t.line,o=t.dot;n.style.left=this.left+"px",n.style.top=this.top+"px",s.style.left=e.line.left+"px","top"==i?(s.style.top="0px",s.style.height=this.top+"px"):(s.style.top=this.top+this.height+"px",s.style.height=Math.max(this.parent.height-this.top-this.height+this.props.dot.height/2,0)+"px"),o.style.left=e.dot.left+"px",o.style.top=e.dot.top+"px"}},_.prototype=new b(null,null),_.prototype.select=function(){this.selected=!0},_.prototype.unselect=function(){this.selected=!1},_.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw Error("Cannot repaint item: no parent attached"); +var i=this.parent.getForeground();if(!i)throw Error("Cannot repaint time axis: parent has no foreground container element");if(e.point.parentNode||(i.appendChild(e.point),i.appendChild(e.point),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var n=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=n&&(this.className=n,e.point.className="item point"+n,t=!0)}return t},_.prototype.show=function(){return this.dom&&this.dom.point.parentNode?!1:this.repaint()},_.prototype.hide=function(){var t=!1,e=this.dom;return e&&e.point.parentNode&&(e.point.parentNode.removeChild(e.point),t=!0),t},_.prototype.reflow=function(){var t,e,i,n,s,o,r,a,h,d,l=0;if(void 0==this.data.start)throw Error('Property "start" missing in item '+this.data.id);if(h=this.data,d=this.parent&&this.parent.range,h&&d){var p=d.end-d.start;this.visible=h.start>d.start-p&&h.start0},_.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.point=document.createElement("div"),t.content=document.createElement("div"),t.content.className="content",t.point.appendChild(t.content),t.dot=document.createElement("div"),t.dot.className="dot",t.point.appendChild(t.dot))},_.prototype.reposition=function(){var t=this.dom,e=this.props;t&&(t.point.style.top=this.top+"px",t.point.style.left=this.left+"px",t.content.style.marginLeft=e.content.marginLeft+"px",t.dot.style.top=e.dot.top+"px")},E.prototype=new b(null,null),E.prototype.select=function(){this.selected=!0},E.prototype.unselect=function(){this.selected=!1},E.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw Error("Cannot repaint time axis: parent has no foreground container element");if(e.box.parentNode||(i.appendChild(e.box),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var n=this.data.className?" "+this.data.className:"";this.className!=n&&(this.className=n,e.box.className="item range"+n,t=!0)}return t},E.prototype.show=function(){return this.dom&&this.dom.box.parentNode?!1:this.repaint()},E.prototype.hide=function(){var t=!1,e=this.dom;return e&&e.box.parentNode&&(e.box.parentNode.removeChild(e.box),t=!0),t},E.prototype.reflow=function(){var t,e,i,n,s,o,r,a,h,d,l,p,c,u,f,m,g=0;if(void 0==this.data.start)throw Error('Property "start" missing in item '+this.data.id);if(void 0==this.data.end)throw Error('Property "end" missing in item '+this.data.id);return h=this.data,d=this.parent&&this.parent.range,this.visible=h&&d?h.startd.start:!1,this.visible&&(t=this.dom,t?(e=this.props,i=this.options,o=this.parent,r=o.toScreen(this.data.start),a=o.toScreen(this.data.end),l=A.updateProperty,p=t.box,c=o.width,f=i.orientation||this.defaultOptions.orientation,n=i.margin&&i.margin.axis||this.defaultOptions.margin.axis,s=i.padding||this.defaultOptions.padding,g+=l(e.content,"width",t.content.offsetWidth),g+=l(this,"height",p.offsetHeight),-c>r&&(r=-c),a>2*c&&(a=2*c),u=0>r?Math.min(-r,a-r-e.content.width-2*s):0,g+=l(e.content,"left",u),"top"==f?(m=n,g+=l(this,"top",m)):(m=o.height-this.height-n,g+=l(this,"top",m)),g+=l(this,"left",r),g+=l(this,"width",Math.max(a-r,1))):g+=1),g>0},E.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.box=document.createElement("div"),t.content=document.createElement("div"),t.content.className="content",t.box.appendChild(t.content))},E.prototype.reposition=function(){var t=this.dom,e=this.props;t&&(t.box.style.top=this.top+"px",t.box.style.left=this.left+"px",t.box.style.width=this.width+"px",t.content.style.left=e.content.left+"px")},T.prototype=new E(null,null),T.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw Error("Cannot repaint time axis: parent has no foreground container element");if(e.box.parentNode||(i.appendChild(e.box),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var n=this.data.className?" "+this.data.className:"";this.className!=n&&(this.className=n,e.box.className="item rangeoverflow"+n,t=!0)}return t},T.prototype.getWidth=function(){return void 0!==this.props.content&&this.width0},S.prototype=new u,S.prototype.setOptions=c.prototype.setOptions,S.prototype.setRange=function(){},S.prototype.setItems=function(t){this.itemsData=t;for(var e in this.groups)if(this.groups.hasOwnProperty(e)){var i=this.groups[e];i.setItems(t)}},S.prototype.getItems=function(){return this.itemsData},S.prototype.setRange=function(t){this.range=t},S.prototype.setGroups=function(t){var e,i=this;if(this.groupsData&&(A.forEach(this.listeners,function(t,e){i.groupsData.unsubscribe(e,t)}),e=this.groupsData.getIds(),this._onRemove(e)),t?t instanceof o?this.groupsData=t:(this.groupsData=new o({convert:{start:"Date",end:"Date"}}),this.groupsData.add(t)):this.groupsData=null,this.groupsData){var n=this.id;A.forEach(this.listeners,function(t,e){i.groupsData.subscribe(e,t,n)}),e=this.groupsData.getIds(),this._onAdd(e)}},S.prototype.getGroups=function(){return this.groupsData},S.prototype.repaint=function(){var t,e,i,n,s=0,o=A.updateProperty,r=A.option.asSize,a=A.option.asElement,h=this.options,d=this.dom.frame,l=this.dom.labels,p=this.dom.labelSet;if(!this.parent)throw Error("Cannot repaint groupset: no parent attached");var c=this.parent.getContainer();if(!c)throw Error("Cannot repaint groupset: parent has no container element");if(!d){d=document.createElement("div"),d.className="groupset",this.dom.frame=d;var u=h.className;u&&A.addClassName(d,A.option.asString(u)),s+=1}d.parentNode||(c.appendChild(d),s+=1);var f=a(h.labelContainer);if(!f)throw Error('Cannot repaint groupset: option "labelContainer" not defined');l||(l=document.createElement("div"),l.className="labels",this.dom.labels=l),p||(p=document.createElement("div"),p.className="label-set",l.appendChild(p),this.dom.labelSet=p),l.parentNode&&l.parentNode==f||(l.parentNode&&l.parentNode.removeChild(l.parentNode),f.appendChild(l)),s+=o(d.style,"height",r(h.height,this.height+"px")),s+=o(d.style,"top",r(h.top,"0px")),s+=o(d.style,"left",r(h.left,"0px")),s+=o(d.style,"width",r(h.width,"100%")),s+=o(p.style,"top",r(h.top,"0px")),s+=o(p.style,"height",r(h.height,this.height+"px"));var m=this,g=this.queue,v=this.groups,y=this.groupsData,b=Object.keys(g);if(b.length){b.forEach(function(t){var e=g[t],i=v[t];switch(e){case"add":case"update":if(!i){var n=Object.create(m.options);A.extend(n,{height:null,maxHeight:null}),i=new x(m,t,n),i.setItems(m.itemsData),v[t]=i,m.controller.add(i)}i.data=y.get(t),delete g[t];break;case"remove":i&&(i.setItems(),delete v[t],m.controller.remove(i)),delete g[t];break;default:console.log('Error: unknown action "'+e+'"')}});var w=this.groupsData.getIds({order:this.options.groupOrder});for(t=0;w.length>t;t++)(function(t,e){var i=0;e&&(i=function(){return e.top+e.height}),t.setOptions({top:i})})(v[w[t]],v[w[t-1]]);for(;p.firstChild;)p.removeChild(p.firstChild);for(t=0;w.length>t;t++)e=w[t],n=this._createLabel(e),p.appendChild(n);s++}for(e in v)v.hasOwnProperty(e)&&(i=v[e],n=i.label,n&&(n.style.top=i.top+"px",n.style.height=i.height+"px"));return s>0},S.prototype._createLabel=function(t){var e=this.groups[t],i=document.createElement("div");i.className="label";var n=document.createElement("div");n.className="inner",i.appendChild(n);var s=e.data&&e.data.content;s instanceof Element?n.appendChild(s):void 0!=s&&(n.innerHTML=s);var o=e.data&&e.data.className;return o&&A.addClassName(i,o),e.label=i,i},S.prototype.getContainer=function(){return this.dom.frame},S.prototype.getLabelsWidth=function(){return this.props.labels.width},S.prototype.reflow=function(){var t,e,i=0,n=this.options,s=A.updateProperty,o=A.option.asNumber,r=A.option.asSize,a=this.dom.frame;if(a){var h,d=o(n.maxHeight),l=null!=r(n.height);if(l)h=a.offsetHeight;else{h=0;for(t in this.groups)this.groups.hasOwnProperty(t)&&(e=this.groups[t],h+=e.height)}null!=d&&(h=Math.min(h,d)),i+=s(this,"height",h),i+=s(this,"top",a.offsetTop),i+=s(this,"left",a.offsetLeft),i+=s(this,"width",a.offsetWidth)}var p=0;for(t in this.groups)if(this.groups.hasOwnProperty(t)){e=this.groups[t];var c=e.props&&e.props.label&&e.props.label.width||0;p=Math.max(p,c)}return i+=s(this.props.labels,"width",p),i>0},S.prototype.hide=function(){return this.dom.frame&&this.dom.frame.parentNode?(this.dom.frame.parentNode.removeChild(this.dom.frame),!0):!1},S.prototype.show=function(){return this.dom.frame&&this.dom.frame.parentNode?!1:this.repaint()},S.prototype._onUpdate=function(t){this._toQueue(t,"update")},S.prototype._onAdd=function(t){this._toQueue(t,"add")},S.prototype._onRemove=function(t){this._toQueue(t,"remove")},S.prototype._toQueue=function(t,e){var i=this.queue;t.forEach(function(t){i[t]=e}),this.controller&&this.requestRepaint()},D.prototype.setOptions=function(t){A.extend(this.options,t),this.range.setRange(),this.controller.reflow(),this.controller.repaint()},D.prototype.setCustomTime=function(t){this.customtime._setCustomTime(t)},D.prototype.getCustomTime=function(){return new Date(this.customtime.customTime.valueOf())},D.prototype.setItems=function(t){var e,i=null==this.itemsData;if(t?t instanceof o&&(e=t):e=null,t instanceof o||(e=new o({convert:{start:"Date",end:"Date"}}),e.add(t)),this.itemsData=e,this.content.setItems(e),i&&(void 0==this.options.start||void 0==this.options.end)){var n=this.getItemRange(),s=n.min,r=n.max;if(null!=s&&null!=r){var a=r.valueOf()-s.valueOf();0>=a&&(a=864e5),s=new Date(s.valueOf()-.05*a),r=new Date(r.valueOf()+.05*a)}void 0!=this.options.start&&(s=A.convert(this.options.start,"Date")),void 0!=this.options.end&&(r=A.convert(this.options.end,"Date")),(null!=s||null!=r)&&this.range.setRange(s,r)}},D.prototype.setGroups=function(t){var e=this;this.groupsData=t;var i=this.groupsData?S:y;if(!(this.content instanceof i)){this.content&&(this.content.hide(),this.content.setItems&&this.content.setItems(),this.content.setGroups&&this.content.setGroups(),this.controller.remove(this.content));var n=Object.create(this.options);A.extend(n,{top:function(){return"top"==e.options.orientation?e.timeaxis.height:e.itemPanel.height-e.timeaxis.height-e.content.height},left:null,width:"100%",height:function(){return e.options.height?e.itemPanel.height-e.timeaxis.height:null},maxHeight:function(){if(e.options.maxHeight){if(!A.isNumber(e.options.maxHeight))throw new TypeError("Number expected for property maxHeight");return e.options.maxHeight-e.timeaxis.height}return null},labelContainer:function(){return e.labelPanel.getContainer()}}),this.content=new i(this.itemPanel,[this.timeaxis],n),this.content.setRange&&this.content.setRange(this.range),this.content.setItems&&this.content.setItems(this.itemsData),this.content.setGroups&&this.content.setGroups(this.groupsData),this.controller.add(this.content)}},D.prototype.getItemRange=function(){var t=this.itemsData,e=null,i=null;if(t){var n=t.min("start");e=n?n.start.valueOf():null;var s=t.max("start");s&&(i=s.start.valueOf());var o=t.max("end");o&&(i=null==i?o.end.valueOf():Math.max(i,o.end.valueOf()))}return{min:null!=e?new Date(e):null,max:null!=i?new Date(i):null}},function(t){function e(t){return D=t,c()}function i(){M=0,C=D.charAt(0)}function n(){M++,C=D.charAt(M)}function s(){return D.charAt(M+1)}function o(t){return L.test(t)}function r(t,e){if(t||(t={}),e)for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return t}function a(t,e,i){for(var n=e.split("."),s=t;n.length;){var o=n.shift();n.length?(s[o]||(s[o]={}),s=s[o]):s[o]=i}}function h(t,e){for(var i,n,s=null,o=[t],a=t;a.parent;)o.push(a.parent),a=a.parent;if(a.nodes)for(i=0,n=a.nodes.length;n>i;i++)if(e.id===a.nodes[i].id){s=a.nodes[i];break}for(s||(s={id:e.id},t.node&&(s.attr=r(s.attr,t.node))),i=o.length-1;i>=0;i--){var h=o[i];h.nodes||(h.nodes=[]),-1==h.nodes.indexOf(s)&&h.nodes.push(s)}e.attr&&(s.attr=r(s.attr,e.attr))}function d(t,e){if(t.edges||(t.edges=[]),t.edges.push(e),t.edge){var i=r({},t.edge);e.attr=r(i,e.attr)}}function l(t,e,i,n,s){var o={from:e,to:i,type:n};return t.edge&&(o.attr=r({},t.edge)),o.attr=r(o.attr||{},s),o}function p(){for(N=x.NULL,O="";" "==C||" "==C||"\n"==C||"\r"==C;)n();do{var t=!1;if("#"==C){for(var e=M-1;" "==D.charAt(e)||" "==D.charAt(e);)e--;if("\n"==D.charAt(e)||""==D.charAt(e)){for(;""!=C&&"\n"!=C;)n();t=!0}}if("/"==C&&"/"==s()){for(;""!=C&&"\n"!=C;)n();t=!0}if("/"==C&&"*"==s()){for(;""!=C;){if("*"==C&&"/"==s()){n(),n();break}n()}t=!0}for(;" "==C||" "==C||"\n"==C||"\r"==C;)n()}while(t);if(""==C)return N=x.DELIMITER,void 0;var i=C+s();if(S[i])return N=x.DELIMITER,O=i,n(),n(),void 0;if(S[C])return N=x.DELIMITER,O=C,n(),void 0;if(o(C)||"-"==C){for(O+=C,n();o(C);)O+=C,n();return"false"==O?O=!1:"true"==O?O=!0:isNaN(Number(O))||(O=Number(O)),N=x.IDENTIFIER,void 0}if('"'==C){for(n();""!=C&&('"'!=C||'"'==C&&'"'==s());)O+=C,'"'==C&&n(),n();if('"'!=C)throw w('End of string " expected');return n(),N=x.IDENTIFIER,void 0}for(N=x.UNKNOWN;""!=C;)O+=C,n();throw new SyntaxError('Syntax error in part "'+_(O,30)+'"')}function c(){var t={};if(i(),p(),"strict"==O&&(t.strict=!0,p()),("graph"==O||"digraph"==O)&&(t.type=O,p()),N==x.IDENTIFIER&&(t.id=O,p()),"{"!=O)throw w("Angle bracket { expected");if(p(),u(t),"}"!=O)throw w("Angle bracket } expected");if(p(),""!==O)throw w("End of file expected");return p(),delete t.node,delete t.edge,delete t.graph,t}function u(t){for(;""!==O&&"}"!=O;)f(t),";"==O&&p()}function f(t){var e=m(t);if(e)return y(t,e),void 0;var i=g(t);if(!i){if(N!=x.IDENTIFIER)throw w("Identifier expected");var n=O;if(p(),"="==O){if(p(),N!=x.IDENTIFIER)throw w("Identifier expected");t[n]=O,p()}else v(t,n)}}function m(t){var e=null;if("subgraph"==O&&(e={},e.type="subgraph",p(),N==x.IDENTIFIER&&(e.id=O,p())),"{"==O){if(p(),e||(e={}),e.parent=t,e.node=t.node,e.edge=t.edge,e.graph=t.graph,u(e),"}"!=O)throw w("Angle bracket } expected");p(),delete e.node,delete e.edge,delete e.graph,delete e.parent,t.subgraphs||(t.subgraphs=[]),t.subgraphs.push(e)}return e}function g(t){return"node"==O?(p(),t.node=b(),"node"):"edge"==O?(p(),t.edge=b(),"edge"):"graph"==O?(p(),t.graph=b(),"graph"):null}function v(t,e){var i={id:e},n=b();n&&(i.attr=n),h(t,i),y(t,e)}function y(t,e){for(;"->"==O||"--"==O;){var i,n=O;p();var s=m(t);if(s)i=s;else{if(N!=x.IDENTIFIER)throw w("Identifier or subgraph expected");i=O,h(t,{id:i}),p()}var o=b(),r=l(t,e,i,n,o);d(t,r),e=i}}function b(){for(var t=null;"["==O;){for(p(),t={};""!==O&&"]"!=O;){if(N!=x.IDENTIFIER)throw w("Attribute name expected");var e=O;if(p(),"="!=O)throw w("Equal sign = expected");if(p(),N!=x.IDENTIFIER)throw w("Attribute value expected");var i=O;a(t,e,i),p(),","==O&&p()}if("]"!=O)throw w("Bracket ] expected");p()}return t}function w(t){return new SyntaxError(t+', got "'+_(O,30)+'" (char '+M+")")}function _(t,e){return e>=t.length?t:t.substr(0,27)+"..."}function E(t,e,i){t instanceof Array?t.forEach(function(t){e instanceof Array?e.forEach(function(e){i(t,e)}):i(t,e)}):e instanceof Array?e.forEach(function(e){i(t,e)}):i(t,e)}function T(t){function i(t){var e={from:t.from,to:t.to};return r(e,t.attr),e.style="->"==t.type?"arrow":"line",e}var n=e(t),s={nodes:[],edges:[],options:{}};return n.nodes&&n.nodes.forEach(function(t){var e={id:t.id,label:(t.label||t.id)+""};r(e,t.attr),e.image&&(e.shape="image"),s.nodes.push(e)}),n.edges&&n.edges.forEach(function(t){var e,n;e=t.from instanceof Object?t.from.nodes:{id:t.from},n=t.to instanceof Object?t.to.nodes:{id:t.to},t.from instanceof Object&&t.from.edges&&t.from.edges.forEach(function(t){var e=i(t);s.edges.push(e)}),E(e,n,function(e,n){var o=l(s,e.id,n.id,t.type,t.attr),r=i(o);s.edges.push(r)}),t.to instanceof Object&&t.to.edges&&t.to.edges.forEach(function(t){var e=i(t);s.edges.push(e)})}),n.attr&&(s.options=n.attr),s}var x={NULL:0,DELIMITER:1,IDENTIFIER:2,UNKNOWN:3},S={"{":!0,"}":!0,"[":!0,"]":!0,";":!0,"=":!0,",":!0,"->":!0,"--":!0},D="",M=0,C="",O="",N=x.NULL,L=/[a-zA-Z_0-9.:#]/;t.parseDOT=e,t.DOTToGraph=T}(A!==void 0?A:n),"undefined"!=typeof CanvasRenderingContext2D&&(CanvasRenderingContext2D.prototype.circle=function(t,e,i){this.beginPath(),this.arc(t,e,i,0,2*Math.PI,!1)},CanvasRenderingContext2D.prototype.square=function(t,e,i){this.beginPath(),this.rect(t-i,e-i,2*i,2*i)},CanvasRenderingContext2D.prototype.triangle=function(t,e,i){this.beginPath();var n=2*i,s=n/2,o=Math.sqrt(3)/6*n,r=Math.sqrt(n*n-s*s);this.moveTo(t,e-(r-o)),this.lineTo(t+s,e+o),this.lineTo(t-s,e+o),this.lineTo(t,e-(r-o)),this.closePath()},CanvasRenderingContext2D.prototype.triangleDown=function(t,e,i){this.beginPath();var n=2*i,s=n/2,o=Math.sqrt(3)/6*n,r=Math.sqrt(n*n-s*s);this.moveTo(t,e+(r-o)),this.lineTo(t+s,e-o),this.lineTo(t-s,e-o),this.lineTo(t,e+(r-o)),this.closePath()},CanvasRenderingContext2D.prototype.star=function(t,e,i){this.beginPath();for(var n=0;10>n;n++){var s=0===n%2?1.3*i:.5*i;this.lineTo(t+s*Math.sin(2*n*Math.PI/10),e-s*Math.cos(2*n*Math.PI/10))}this.closePath()},CanvasRenderingContext2D.prototype.roundRect=function(t,e,i,n,s){var o=Math.PI/180;0>i-2*s&&(s=i/2),0>n-2*s&&(s=n/2),this.beginPath(),this.moveTo(t+s,e),this.lineTo(t+i-s,e),this.arc(t+i-s,e+s,s,270*o,360*o,!1),this.lineTo(t+i,e+n-s),this.arc(t+i-s,e+n-s,s,0,90*o,!1),this.lineTo(t+s,e+n),this.arc(t+s,e+n-s,s,90*o,180*o,!1),this.lineTo(t,e+s),this.arc(t+s,e+s,s,180*o,270*o,!1)},CanvasRenderingContext2D.prototype.ellipse=function(t,e,i,n){var s=.5522848,o=i/2*s,r=n/2*s,a=t+i,h=e+n,d=t+i/2,l=e+n/2;this.beginPath(),this.moveTo(t,l),this.bezierCurveTo(t,l-r,d-o,e,d,e),this.bezierCurveTo(d+o,e,a,l-r,a,l),this.bezierCurveTo(a,l+r,d+o,h,d,h),this.bezierCurveTo(d-o,h,t,l+r,t,l)},CanvasRenderingContext2D.prototype.database=function(t,e,i,n){var s=1/3,o=i,r=n*s,a=.5522848,h=o/2*a,d=r/2*a,l=t+o,p=e+r,c=t+o/2,u=e+r/2,f=e+(n-r/2),m=e+n;this.beginPath(),this.moveTo(l,u),this.bezierCurveTo(l,u+d,c+h,p,c,p),this.bezierCurveTo(c-h,p,t,u+d,t,u),this.bezierCurveTo(t,u-d,c-h,e,c,e),this.bezierCurveTo(c+h,e,l,u-d,l,u),this.lineTo(l,f),this.bezierCurveTo(l,f+d,c+h,m,c,m),this.bezierCurveTo(c-h,m,t,f+d,t,f),this.lineTo(t,u)},CanvasRenderingContext2D.prototype.arrow=function(t,e,i,n){var s=t-n*Math.cos(i),o=e-n*Math.sin(i),r=t-.9*n*Math.cos(i),a=e-.9*n*Math.sin(i),h=s+n/3*Math.cos(i+.5*Math.PI),d=o+n/3*Math.sin(i+.5*Math.PI),l=s+n/3*Math.cos(i-.5*Math.PI),p=o+n/3*Math.sin(i-.5*Math.PI);this.beginPath(),this.moveTo(t,e),this.lineTo(h,d),this.lineTo(r,a),this.lineTo(l,p),this.closePath()},CanvasRenderingContext2D.prototype.dashedLine=function(t,e,i,n,s){s||(s=[10,5]),0==c&&(c=.001);var o=s.length;this.moveTo(t,e);for(var r=i-t,a=n-e,h=a/r,d=Math.sqrt(r*r+a*a),l=0,p=!0;d>=.1;){var c=s[l++%o];c>d&&(c=d);var u=Math.sqrt(c*c/(1+h*h));0>r&&(u=-u),t+=u,e+=h*u,this[p?"lineTo":"moveTo"](t,e),d-=c,p=!p}}),M.prototype.attachEdge=function(t){-1==this.edges.indexOf(t)&&this.edges.push(t),this._updateMass()},M.prototype.detachEdge=function(t){var e=this.edges.indexOf(t);-1!=e&&this.edges.splice(e,1),this._updateMass()},M.prototype._updateMass=function(){this.mass=50+20*this.edges.length},M.prototype.setProperties=function(t,e){if(t){if(void 0!=t.id&&(this.id=t.id),void 0!=t.label&&(this.label=t.label),void 0!=t.title&&(this.title=t.title),void 0!=t.group&&(this.group=t.group),void 0!=t.x&&(this.x=t.x),void 0!=t.y&&(this.y=t.y),void 0!=t.value&&(this.value=t.value),void 0===this.id)throw"Node must have an id";if(this.group){var i=this.grouplist.get(this.group);for(var n in i)i.hasOwnProperty(n)&&(this[n]=i[n])}if(void 0!=t.shape&&(this.shape=t.shape),void 0!=t.image&&(this.image=t.image),void 0!=t.radius&&(this.radius=t.radius),void 0!=t.color&&(this.color=M.parseColor(t.color)),void 0!=t.fontColor&&(this.fontColor=t.fontColor),void 0!=t.fontSize&&(this.fontSize=t.fontSize),void 0!=t.fontFace&&(this.fontFace=t.fontFace),void 0!=this.image){if(!this.imagelist)throw"No imagelist provided";this.imageObj=this.imagelist.load(this.image)}switch(this.xFixed=this.xFixed||void 0!=t.x,this.yFixed=this.yFixed||void 0!=t.y,this.radiusFixed=this.radiusFixed||void 0!=t.radius,"image"==this.shape&&(this.radiusMin=e.nodes.widthMin,this.radiusMax=e.nodes.widthMax),this.shape){case"database":this.draw=this._drawDatabase,this.resize=this._resizeDatabase;break;case"box":this.draw=this._drawBox,this.resize=this._resizeBox;break;case"circle":this.draw=this._drawCircle,this.resize=this._resizeCircle;break;case"ellipse":this.draw=this._drawEllipse,this.resize=this._resizeEllipse;break;case"image":this.draw=this._drawImage,this.resize=this._resizeImage;break;case"text":this.draw=this._drawText,this.resize=this._resizeText;break;case"dot":this.draw=this._drawDot,this.resize=this._resizeShape;break;case"square":this.draw=this._drawSquare,this.resize=this._resizeShape;break;case"triangle":this.draw=this._drawTriangle,this.resize=this._resizeShape;break;case"triangleDown":this.draw=this._drawTriangleDown,this.resize=this._resizeShape;break;case"star":this.draw=this._drawStar,this.resize=this._resizeShape;break;default:this.draw=this._drawEllipse,this.resize=this._resizeEllipse}this._reset()}},M.parseColor=function(t){var e;return A.isString(t)?e={border:t,background:t,highlight:{border:t,background:t}}:(e={},e.background=t.background||"white",e.border=t.border||e.background,A.isString(t.highlight)?e.highlight={border:t.highlight,background:t.highlight}:(e.highlight={},e.highlight.background=t.highlight&&t.highlight.background||e.background,e.highlight.border=t.highlight&&t.highlight.border||e.border)),e},M.prototype.select=function(){this.selected=!0,this._reset()},M.prototype.unselect=function(){this.selected=!1,this._reset()},M.prototype._reset=function(){this.width=void 0,this.height=void 0},M.prototype.getTitle=function(){return this.title},M.prototype.distanceToBorder=function(t,e){var i=1;switch(this.width||this.resize(t),this.shape){case"circle":case"dot":return this.radius+i;case"ellipse":var n=this.width/2,s=this.height/2,o=Math.sin(e)*n,r=Math.cos(e)*s;return n*s/Math.sqrt(o*o+r*r);case"box":case"image":case"text":default:return this.width?Math.min(Math.abs(this.width/2/Math.cos(e)),Math.abs(this.height/2/Math.sin(e)))+i:0}},M.prototype._setForce=function(t,e){this.fx=t,this.fy=e},M.prototype._addForce=function(t,e){this.fx+=t,this.fy+=e},M.prototype.discreteStep=function(t){if(!this.xFixed){var e=-this.damping*this.vx,i=(this.fx+e)/this.mass;this.vx+=i/t,this.x+=this.vx/t}if(!this.yFixed){var n=-this.damping*this.vy,s=(this.fy+n)/this.mass;this.vy+=s/t,this.y+=this.vy/t}},M.prototype.isFixed=function(){return this.xFixed&&this.yFixed},M.prototype.isMoving=function(t){return Math.abs(this.vx)>t||Math.abs(this.vy)>t||!this.xFixed&&Math.abs(this.fx)>this.minForce||!this.yFixed&&Math.abs(this.fy)>this.minForce},M.prototype.isSelected=function(){return this.selected},M.prototype.getValue=function(){return this.value},M.prototype.getDistance=function(t,e){var i=this.x-t,n=this.y-e;return Math.sqrt(i*i+n*n)},M.prototype.setValueRange=function(t,e){if(!this.radiusFixed&&void 0!==this.value)if(e==t)this.radius=(this.radiusMin+this.radiusMax)/2;else{var i=(this.radiusMax-this.radiusMin)/(e-t);this.radius=(this.value-t)*i+this.radiusMin}},M.prototype.draw=function(){throw"Draw method not initialized for node"},M.prototype.resize=function(){throw"Resize method not initialized for node"},M.prototype.isOverlappingWith=function(t){return this.leftt.left&&this.topt.top},M.prototype._resizeImage=function(){if(!this.width){var t,e;if(this.value){var i=this.imageObj.height/this.imageObj.width;t=this.radius||this.imageObj.width,e=this.radius*i||this.imageObj.height}else t=this.imageObj.width,e=this.imageObj.height;this.width=t,this.height=e}},M.prototype._drawImage=function(t){this._resizeImage(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e;this.imageObj?(t.drawImage(this.imageObj,this.left,this.top,this.width,this.height),e=this.y+this.height/2):e=this.y,this._label(t,this.label,this.x,e,void 0,"top")},M.prototype._resizeBox=function(t){if(!this.width){var e=5,i=this.getTextSize(t);this.width=i.width+2*e,this.height=i.height+2*e}},M.prototype._drawBox=function(t){this._resizeBox(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.lineWidth=this.selected?2:1,t.roundRect(this.left,this.top,this.width,this.height,this.radius),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},M.prototype._resizeDatabase=function(t){if(!this.width){var e=5,i=this.getTextSize(t),n=i.width+2*e;this.width=n,this.height=n}},M.prototype._drawDatabase=function(t){this._resizeDatabase(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.lineWidth=this.selected?2:1,t.database(this.x-this.width/2,this.y-.5*this.height,this.width,this.height),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},M.prototype._resizeCircle=function(t){if(!this.width){var e=5,i=this.getTextSize(t),n=Math.max(i.width,i.height)+2*e;this.radius=n/2,this.width=n,this.height=n}},M.prototype._drawCircle=function(t){this._resizeCircle(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.lineWidth=this.selected?2:1,t.circle(this.x,this.y,this.radius),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},M.prototype._resizeEllipse=function(t){if(!this.width){var e=this.getTextSize(t);this.width=1.5*e.width,this.height=2*e.height,this.widthl;l++)t.fillText(r[l],i,d),d+=h}},M.prototype.getTextSize=function(t){if(void 0!=this.label){t.font=(this.selected?"bold ":"")+this.fontSize+"px "+this.fontFace;for(var e=this.label.split("\n"),i=(this.fontSize+4)*e.length,n=0,s=0,o=e.length;o>s;s++)n=Math.max(n,t.measureText(e[s]).width);return{width:n,height:i}}return{width:0,height:0}},C.prototype.setProperties=function(t,e){if(t)switch(void 0!=t.from&&(this.fromId=t.from),void 0!=t.to&&(this.toId=t.to),void 0!=t.id&&(this.id=t.id),void 0!=t.style&&(this.style=t.style),void 0!=t.label&&(this.label=t.label),this.label&&(this.fontSize=e.edges.fontSize,this.fontFace=e.edges.fontFace,this.fontColor=e.edges.fontColor,void 0!=t.fontColor&&(this.fontColor=t.fontColor),void 0!=t.fontSize&&(this.fontSize=t.fontSize),void 0!=t.fontFace&&(this.fontFace=t.fontFace)),void 0!=t.title&&(this.title=t.title),void 0!=t.width&&(this.width=t.width),void 0!=t.value&&(this.value=t.value),void 0!=t.length&&(this.length=t.length),t.dash&&(void 0!=t.dash.length&&(this.dash.length=t.dash.length),void 0!=t.dash.gap&&(this.dash.gap=t.dash.gap),void 0!=t.dash.altLength&&(this.dash.altLength=t.dash.altLength)),void 0!=t.color&&(this.color=t.color),this.connect(),this.widthFixed=this.widthFixed||void 0!=t.width,this.lengthFixed=this.lengthFixed||void 0!=t.length,this.stiffness=1/this.length,this.style){case"line":this.draw=this._drawLine;break;case"arrow":this.draw=this._drawArrow;break;case"arrow-center":this.draw=this._drawArrowCenter;break;case"dash-line":this.draw=this._drawDashLine;break;default:this.draw=this._drawLine}},C.prototype.connect=function(){this.disconnect(),this.from=this.graph.nodes[this.fromId]||null,this.to=this.graph.nodes[this.toId]||null,this.connected=this.from&&this.to,this.connected?(this.from.attachEdge(this),this.to.attachEdge(this)):(this.from&&this.from.detachEdge(this),this.to&&this.to.detachEdge(this)) +},C.prototype.disconnect=function(){this.from&&(this.from.detachEdge(this),this.from=null),this.to&&(this.to.detachEdge(this),this.to=null),this.connected=!1},C.prototype.getTitle=function(){return this.title},C.prototype.getValue=function(){return this.value},C.prototype.setValueRange=function(t,e){if(!this.widthFixed&&void 0!==this.value){var i=(this.widthMax-this.widthMin)/(e-t);this.width=(this.value-t)*i+this.widthMin}},C.prototype.draw=function(){throw"Method draw not initialized in edge"},C.prototype.isOverlappingWith=function(t){var e=10,i=this.from.x,n=this.from.y,s=this.to.x,o=this.to.y,r=t.left,a=t.top,h=C._dist(i,n,s,o,r,a);return e>h},C.prototype._drawLine=function(t){t.strokeStyle=this.color,t.lineWidth=this._getLineWidth();var e;if(this.from!=this.to)this._line(t),this.label&&(e=this._pointOnLine(.5),this._label(t,this.label,e.x,e.y));else{var i,n,s=this.length/4,o=this.from;o.width||o.resize(t),o.width>o.height?(i=o.x+o.width/2,n=o.y-s):(i=o.x+s,n=o.y-o.height/2),this._circle(t,i,n,s),e=this._pointOnCircle(i,n,s,.5),this._label(t,this.label,e.x,e.y)}},C.prototype._getLineWidth=function(){return this.from.selected||this.to.selected?Math.min(2*this.width,this.widthMax):this.width},C.prototype._line=function(t){t.beginPath(),t.moveTo(this.from.x,this.from.y),t.lineTo(this.to.x,this.to.y),t.stroke()},C.prototype._circle=function(t,e,i,n){t.beginPath(),t.arc(e,i,n,0,2*Math.PI,!1),t.stroke()},C.prototype._label=function(t,e,i,n){if(e){t.font=(this.from.selected||this.to.selected?"bold ":"")+this.fontSize+"px "+this.fontFace,t.fillStyle="white";var s=t.measureText(e).width,o=this.fontSize,r=i-s/2,a=n-o/2;t.fillRect(r,a,s,o),t.fillStyle=this.fontColor||"black",t.textAlign="left",t.textBaseline="top",t.fillText(e,r,a)}},C.prototype._drawDashLine=function(t){if(t.strokeStyle=this.color,t.lineWidth=this._getLineWidth(),t.beginPath(),t.lineCap="round",void 0!=this.dash.altLength?t.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]):void 0!=this.dash.length&&void 0!=this.dash.gap?t.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dash.length,this.dash.gap]):(t.moveTo(this.from.x,this.from.y),t.lineTo(this.to.x,this.to.y)),t.stroke(),this.label){var e=this._pointOnLine(.5);this._label(t,this.label,e.x,e.y)}},C.prototype._pointOnLine=function(t){return{x:(1-t)*this.from.x+t*this.to.x,y:(1-t)*this.from.y+t*this.to.y}},C.prototype._pointOnCircle=function(t,e,i,n){var s=2*(n-3/8)*Math.PI;return{x:t+i*Math.cos(s),y:e-i*Math.sin(s)}},C.prototype._drawArrowCenter=function(t){var e;if(t.strokeStyle=this.color,t.fillStyle=this.color,t.lineWidth=this._getLineWidth(),this.from!=this.to){this._line(t);var i=Math.atan2(this.to.y-this.from.y,this.to.x-this.from.x),n=10+5*this.width;e=this._pointOnLine(.5),t.arrow(e.x,e.y,i,n),t.fill(),t.stroke(),this.label&&(e=this._pointOnLine(.5),this._label(t,this.label,e.x,e.y))}else{var s,o,r=this.length/4,a=this.from;a.width||a.resize(t),a.width>a.height?(s=a.x+a.width/2,o=a.y-r):(s=a.x+r,o=a.y-a.height/2),this._circle(t,s,o,r);var i=.2*Math.PI,n=10+5*this.width;e=this._pointOnCircle(s,o,r,.5),t.arrow(e.x,e.y,i,n),t.fill(),t.stroke(),this.label&&(e=this._pointOnCircle(s,o,r,.5),this._label(t,this.label,e.x,e.y))}},C.prototype._drawArrow=function(t){t.strokeStyle=this.color,t.fillStyle=this.color,t.lineWidth=this._getLineWidth();var e,i;if(this.from!=this.to){e=Math.atan2(this.to.y-this.from.y,this.to.x-this.from.x);var n=this.to.x-this.from.x,s=this.to.y-this.from.y,o=Math.sqrt(n*n+s*s),r=this.from.distanceToBorder(t,e+Math.PI),a=(o-r)/o,h=a*this.from.x+(1-a)*this.to.x,d=a*this.from.y+(1-a)*this.to.y,l=this.to.distanceToBorder(t,e),p=(o-l)/o,c=(1-p)*this.from.x+p*this.to.x,u=(1-p)*this.from.y+p*this.to.y;if(t.beginPath(),t.moveTo(h,d),t.lineTo(c,u),t.stroke(),i=10+5*this.width,t.arrow(c,u,e,i),t.fill(),t.stroke(),this.label){var f=this._pointOnLine(.5);this._label(t,this.label,f.x,f.y)}}else{var m,g,v,y=this.from,b=this.length/4;y.width||y.resize(t),y.width>y.height?(m=y.x+y.width/2,g=y.y-b,v={x:m,y:y.y,angle:.9*Math.PI}):(m=y.x+b,g=y.y-y.height/2,v={x:y.x,y:g,angle:.6*Math.PI}),t.beginPath(),t.arc(m,g,b,0,2*Math.PI,!1),t.stroke(),i=10+5*this.width,t.arrow(v.x,v.y,v.angle,i),t.fill(),t.stroke(),this.label&&(f=this._pointOnCircle(m,g,b,.5),this._label(t,this.label,f.x,f.y))}},C._dist=function(t,e,i,n,s,o){var r=i-t,a=n-e,h=r*r+a*a,d=((s-t)*r+(o-e)*a)/h;d>1?d=1:0>d&&(d=0);var l=t+d*r,p=e+d*a,c=l-s,u=p-o;return Math.sqrt(c*c+u*u)},O.prototype.setPosition=function(t,e){this.x=parseInt(t),this.y=parseInt(e)},O.prototype.setText=function(t){this.frame.innerHTML=t},O.prototype.show=function(t){if(void 0===t&&(t=!0),t){var e=this.frame.clientHeight,i=this.frame.clientWidth,n=this.frame.parentNode.clientHeight,s=this.frame.parentNode.clientWidth,o=this.y-e;o+e+this.padding>n&&(o=n-e-this.padding),this.padding>o&&(o=this.padding);var r=this.x;r+i+this.padding>s&&(r=s-i-this.padding),this.padding>r&&(r=this.padding),this.frame.style.left=r+"px",this.frame.style.top=o+"px",this.frame.style.visibility="visible"}else this.hide()},O.prototype.hide=function(){this.frame.style.visibility="hidden"},Groups=function(){this.clear(),this.defaultIndex=0},Groups.DEFAULT=[{border:"#2B7CE9",background:"#97C2FC",highlight:{border:"#2B7CE9",background:"#D2E5FF"}},{border:"#FFA500",background:"#FFFF00",highlight:{border:"#FFA500",background:"#FFFFA3"}},{border:"#FA0A10",background:"#FB7E81",highlight:{border:"#FA0A10",background:"#FFAFB1"}},{border:"#41A906",background:"#7BE141",highlight:{border:"#41A906",background:"#A1EC76"}},{border:"#E129F0",background:"#EB7DF4",highlight:{border:"#E129F0",background:"#F0B3F5"}},{border:"#7C29F0",background:"#AD85E4",highlight:{border:"#7C29F0",background:"#D3BDF0"}},{border:"#C37F00",background:"#FFA807",highlight:{border:"#C37F00",background:"#FFCA66"}},{border:"#4220FB",background:"#6E6EFD",highlight:{border:"#4220FB",background:"#9B9BFD"}},{border:"#FD5A77",background:"#FFC0CB",highlight:{border:"#FD5A77",background:"#FFD1D9"}},{border:"#4AD63A",background:"#C2FABC",highlight:{border:"#4AD63A",background:"#E6FFE3"}}],Groups.prototype.clear=function(){this.groups={},this.groups.length=function(){var t=0;for(var e in this)this.hasOwnProperty(e)&&t++;return t}},Groups.prototype.get=function(t){var e=this.groups[t];if(void 0==e){var i=this.defaultIndex%Groups.DEFAULT.length;this.defaultIndex++,e={},e.color=Groups.DEFAULT[i],this.groups[t]=e}return e},Groups.prototype.add=function(t,e){return this.groups[t]=e,e.color&&(e.color=M.parseColor(e.color)),e},Images=function(){this.images={},this.callback=void 0},Images.prototype.setOnloadCallback=function(t){this.callback=t},Images.prototype.load=function(t){var e=this.images[t];if(void 0==e){var i=this;e=new Image,this.images[t]=e,e.onload=function(){i.callback&&i.callback(this)},e.src=t}return e},N.prototype.setData=function(t){if(t&&t.dot&&(t.nodes||t.edges))throw new SyntaxError('Data must contain either parameter "dot" or parameter pair "nodes" and "edges", but not both.');if(this.setOptions(t&&t.options),t&&t.dot){if(t&&t.dot){var e=R.util.DOTToGraph(t.dot);return this.setData(e),void 0}}else this._setNodes(t&&t.nodes),this._setEdges(t&&t.edges);this.stabilize&&this._doStabilize(),this.start()},N.prototype.setOptions=function(t){if(t){if(void 0!=t.width&&(this.width=t.width),void 0!=t.height&&(this.height=t.height),void 0!=t.stabilize&&(this.stabilize=t.stabilize),void 0!=t.selectable&&(this.selectable=t.selectable),t.edges){for(var e in t.edges)t.edges.hasOwnProperty(e)&&(this.constants.edges[e]=t.edges[e]);void 0!=t.edges.length&&t.nodes&&void 0==t.nodes.distance&&(this.constants.edges.length=t.edges.length,this.constants.nodes.distance=1.25*t.edges.length),t.edges.fontColor||(this.constants.edges.fontColor=t.edges.color),t.edges.dash&&(void 0!=t.edges.dash.length&&(this.constants.edges.dash.length=t.edges.dash.length),void 0!=t.edges.dash.gap&&(this.constants.edges.dash.gap=t.edges.dash.gap),void 0!=t.edges.dash.altLength&&(this.constants.edges.dash.altLength=t.edges.dash.altLength))}if(t.nodes){for(e in t.nodes)t.nodes.hasOwnProperty(e)&&(this.constants.nodes[e]=t.nodes[e]);t.nodes.color&&(this.constants.nodes.color=M.parseColor(t.nodes.color))}if(t.groups)for(var i in t.groups)if(t.groups.hasOwnProperty(i)){var n=t.groups[i];this.groups.add(i,n)}}this.setSize(this.width,this.height),this._setTranslation(this.frame.clientWidth/2,this.frame.clientHeight/2),this._setScale(1)},N.prototype._trigger=function(t,e){F.trigger(this,t,e)},N.prototype._create=function(){for(;this.containerElement.hasChildNodes();)this.containerElement.removeChild(this.containerElement.firstChild);if(this.frame=document.createElement("div"),this.frame.className="graph-frame",this.frame.style.position="relative",this.frame.style.overflow="hidden",this.frame.canvas=document.createElement("canvas"),this.frame.canvas.style.position="relative",this.frame.appendChild(this.frame.canvas),!this.frame.canvas.getContext){var t=document.createElement("DIV");t.style.color="red",t.style.fontWeight="bold",t.style.padding="10px",t.innerHTML="Error: your browser does not support HTML canvas",this.frame.canvas.appendChild(t)}var e=this;this.drag={},this.pinch={},this.hammer=L(this.frame.canvas,{prevent_default:!0}),this.hammer.on("tap",e._onTap.bind(e)),this.hammer.on("hold",e._onHold.bind(e)),this.hammer.on("pinch",e._onPinch.bind(e)),this.hammer.on("touch",e._onTouch.bind(e)),this.hammer.on("dragstart",e._onDragStart.bind(e)),this.hammer.on("drag",e._onDrag.bind(e)),this.hammer.on("dragend",e._onDragEnd.bind(e)),this.hammer.on("mousewheel",e._onMouseWheel.bind(e)),this.hammer.on("DOMMouseScroll",e._onMouseWheel.bind(e)),this.hammer.on("mousemove",e._onMouseMoveTitle.bind(e)),this.containerElement.appendChild(this.frame)},N.prototype._getNodeAt=function(t){var e=this._canvasToX(t.x),i=this._canvasToY(t.y),n={left:e,top:i,right:e,bottom:i},s=this._getNodesOverlappingWith(n);return s.length>0?s[s.length-1]:null},N.prototype._getPointer=function(t){return{x:t.pageX-R.util.getAbsoluteLeft(this.frame.canvas),y:t.pageY-R.util.getAbsoluteTop(this.frame.canvas)}},N.prototype._onTouch=function(t){this.drag.pointer=this._getPointer(t.gesture.touches[0]),this.drag.pinched=!1,this.pinch.scale=this._getScale()},N.prototype._onDragStart=function(){var t=this.drag;t.selection=[],t.translation=this._getTranslation(),t.nodeId=this._getNodeAt(t.pointer);var e=this.nodes[t.nodeId];if(e){e.isSelected()||this._selectNodes([t.nodeId]);var i=this;this.selection.forEach(function(e){var n=i.nodes[e];if(n){var s={id:e,node:n,x:n.x,y:n.y,xFixed:n.xFixed,yFixed:n.yFixed};n.xFixed=!0,n.yFixed=!0,t.selection.push(s)}})}},N.prototype._onDrag=function(t){if(!this.drag.pinched){var e=this._getPointer(t.gesture.touches[0]),i=this,n=this.drag,s=n.selection;if(s&&s.length){var o=e.x-n.pointer.x,r=e.y-n.pointer.y;s.forEach(function(t){var e=t.node;t.xFixed||(e.x=i._canvasToX(i._xToCanvas(t.x)+o)),t.yFixed||(e.y=i._canvasToY(i._yToCanvas(t.y)+r))}),this.moving||(this.moving=!0,this.start())}else{var a=e.x-this.drag.pointer.x,h=e.y-this.drag.pointer.y;this._setTranslation(this.drag.translation.x+a,this.drag.translation.y+h),this._redraw(),this.moved=!0}}},N.prototype._onDragEnd=function(){var t=this.drag.selection;t&&t.forEach(function(t){t.node.xFixed=t.xFixed,t.node.yFixed=t.yFixed})},N.prototype._onTap=function(t){var e=this._getPointer(t.gesture.touches[0]),i=this._getNodeAt(e),n=this.nodes[i];n?(this._selectNodes([i]),this.moving||this._redraw()):(this._unselectNodes(),this._redraw())},N.prototype._onHold=function(t){var e=this._getPointer(t.gesture.touches[0]),i=this._getNodeAt(e),n=this.nodes[i];if(n){if(n.isSelected())this._unselectNodes([i]);else{var s=!0;this._selectNodes([i],s)}this.moving||this._redraw()}},N.prototype._onPinch=function(t){var e=this._getPointer(t.gesture.center);this.drag.pinched=!0,"scale"in this.pinch||(this.pinch.scale=1);var i=this.pinch.scale*t.gesture.scale;this._zoom(i,e)},N.prototype._zoom=function(t,e){var i=this._getScale();.01>t&&(t=.01),t>10&&(t=10);var n=this._getTranslation(),s=t/i,o=(1-s)*e.x+n.x*s,r=(1-s)*e.y+n.y*s;return this._setScale(t),this._setTranslation(o,r),this._redraw(),t},N.prototype._onMouseWheel=function(t){var e=0;if(t.wheelDelta?e=t.wheelDelta/120:t.detail&&(e=-t.detail/3),e){"mouswheelScale"in this.pinch||(this.pinch.mouswheelScale=1);var i=this.pinch.mouswheelScale,n=e/10;0>e&&(n/=1-n),i*=1+n;var s=L.event.collectEventData(this,"scroll",t),o=this._getPointer(s.center);i=this._zoom(i,o),this.pinch.mouswheelScale=i}t.preventDefault()},N.prototype._onMouseMoveTitle=function(t){var e=L.event.collectEventData(this,"mousemove",t),i=this._getPointer(e.center);this.popupNode&&this._checkHidePopup(i);var n=this,s=function(){n._checkShowPopup(i)};this.popupTimer&&clearInterval(this.popupTimer),this.leftButtonDown||(this.popupTimer=setTimeout(s,300))},N.prototype._checkShowPopup=function(t){var e,i={left:this._canvasToX(t.x),top:this._canvasToY(t.y),right:this._canvasToX(t.x),bottom:this._canvasToY(t.y)},n=this.popupNode;if(void 0==this.popupNode){var s=this.nodes;for(e in s)if(s.hasOwnProperty(e)){var o=s[e];if(void 0!=o.getTitle()&&o.isOverlappingWith(i)){this.popupNode=o;break}}}if(void 0==this.popupNode){var r=this.edges;for(e in r)if(r.hasOwnProperty(e)){var a=r[e];if(a.connected&&void 0!=a.getTitle()&&a.isOverlappingWith(i)){this.popupNode=a;break}}}if(this.popupNode){if(this.popupNode!=n){var h=this;h.popup||(h.popup=new O(h.frame)),h.popup.setPosition(t.x-3,t.y-3),h.popup.setText(h.popupNode.getTitle()),h.popup.show()}}else this.popup&&this.popup.hide()},N.prototype._checkHidePopup=function(t){this.popupNode&&this._getNodeAt(t)||(this.popupNode=void 0,this.popup&&this.popup.hide())},N.prototype._unselectNodes=function(t,e){var i,n,s,o=!1;if(t)for(i=0,n=t.length;n>i;i++){s=t[i],this.nodes[s].unselect();for(var r=0;this.selection.length>r;)this.selection[r]==s?(this.selection.splice(r,1),o=!0):r++}else if(this.selection&&this.selection.length){for(i=0,n=this.selection.length;n>i;i++)s=this.selection[i],this.nodes[s].unselect(),o=!0;this.selection=[]}return!o||1!=e&&void 0!=e||this._trigger("select"),o},N.prototype._selectNodes=function(t,e){var i,n,s=!1,o=!0;if(t.length!=this.selection.length)o=!1;else for(i=0,n=Math.min(t.length,this.selection.length);n>i;i++)if(t[i]!=this.selection[i]){o=!1;break}if(o)return s;if(void 0==e||0==e){var r=!1;s=this._unselectNodes(void 0,r)}for(i=0,n=t.length;n>i;i++){var a=t[i],h=-1!=this.selection.indexOf(a);h||(this.nodes[a].select(),this.selection.push(a),s=!0)}return s&&this._trigger("select"),s},N.prototype._getNodesOverlappingWith=function(t){var e=this.nodes,i=[];for(var n in e)e.hasOwnProperty(n)&&e[n].isOverlappingWith(t)&&i.push(n);return i},N.prototype.getSelection=function(){return this.selection.concat([])},N.prototype.setSelection=function(t){var e,i,n;if(!t||void 0==t.length)throw"Selection must be an array with ids";for(e=0,i=this.selection.length;i>e;e++)n=this.selection[e],this.nodes[n].unselect();for(this.selection=[],e=0,i=t.length;i>e;e++){n=t[e];var s=this.nodes[n];if(!s)throw new RangeError('Node with id "'+n+'" not found');s.select(),this.selection.push(n)}this.redraw()},N.prototype._updateSelection=function(){for(var t=0;this.selection.length>t;){var e=this.selection[t];this.nodes[e]?t++:this.selection.splice(t,1)}},N.prototype._getConnectionCount=function(t){function e(t){for(var e=[],i=0,n=t.length;n>i;i++)for(var s=t[i],o=s.edges,r=0,a=o.length;a>r;r++){var h=o[r],d=null;h.from==s?d=h.to:h.to==s&&(d=h.from);var l,p;if(d)for(l=0,p=t.length;p>l;l++)if(t[l]==d){d=null;break}if(d)for(l=0,p=e.length;p>l;l++)if(e[l]==d){d=null;break}d&&e.push(d)}return e}void 0==t&&(t=1);var i=[],n=this.nodes;for(var s in n)if(n.hasOwnProperty(s)){for(var o=[n[s]],r=0;t>r;r++)o=o.concat(e(o));i.push(o)}for(var a=[],h=0,d=i.length;d>h;h++)a.push(i[h].length);return a},N.prototype.setSize=function(t,e){this.frame.style.width=t,this.frame.style.height=e,this.frame.canvas.style.width="100%",this.frame.canvas.style.height="100%",this.frame.canvas.width=this.frame.canvas.clientWidth,this.frame.canvas.height=this.frame.canvas.clientHeight},N.prototype._setNodes=function(t){var e=this.nodesData;if(t instanceof o||t instanceof r)this.nodesData=t;else if(t instanceof Array)this.nodesData=new o,this.nodesData.add(t);else{if(t)throw new TypeError("Array or DataSet expected");this.nodesData=new o}if(e&&A.forEach(this.nodesListeners,function(t,i){e.unsubscribe(i,t)}),this.nodes={},this.nodesData){var i=this;A.forEach(this.nodesListeners,function(t,e){i.nodesData.subscribe(e,t)});var n=this.nodesData.getIds();this._addNodes(n)}this._updateSelection()},N.prototype._addNodes=function(t){for(var e,i=0,n=t.length;n>i;i++){e=t[i];var s=this.nodesData.get(e),o=new M(s,this.images,this.groups,this.constants);if(this.nodes[e]=o,!o.isFixed()){var r=2*this.constants.edges.length,a=t.length,h=2*Math.PI*(i/a);o.x=r*Math.cos(h),o.y=r*Math.sin(h),this.moving=!0}}this._reconnectEdges(),this._updateValueRange(this.nodes)},N.prototype._updateNodes=function(t){for(var e=this.nodes,i=this.nodesData,n=0,s=t.length;s>n;n++){var o=t[n],r=e[o],a=i.get(o);r?r.setProperties(a,this.constants):(r=new M(properties,this.images,this.groups,this.constants),e[o]=r,r.isFixed()||(this.moving=!0))}this._reconnectEdges(),this._updateValueRange(e)},N.prototype._removeNodes=function(t){for(var e=this.nodes,i=0,n=t.length;n>i;i++){var s=t[i];delete e[s]}this._reconnectEdges(),this._updateSelection(),this._updateValueRange(e)},N.prototype._setEdges=function(t){var e=this.edgesData;if(t instanceof o||t instanceof r)this.edgesData=t;else if(t instanceof Array)this.edgesData=new o,this.edgesData.add(t);else{if(t)throw new TypeError("Array or DataSet expected");this.edgesData=new o}if(e&&A.forEach(this.edgesListeners,function(t,i){e.unsubscribe(i,t)}),this.edges={},this.edgesData){var i=this;A.forEach(this.edgesListeners,function(t,e){i.edgesData.subscribe(e,t)});var n=this.edgesData.getIds();this._addEdges(n)}this._reconnectEdges()},N.prototype._addEdges=function(t){for(var e=this.edges,i=this.edgesData,n=0,s=t.length;s>n;n++){var o=t[n],r=e[o];r&&r.disconnect();var a=i.get(o);e[o]=new C(a,this,this.constants)}this.moving=!0,this._updateValueRange(e)},N.prototype._updateEdges=function(t){for(var e=this.edges,i=this.edgesData,n=0,s=t.length;s>n;n++){var o=t[n],r=i.get(o),a=e[o];a?(a.disconnect(),a.setProperties(r,this.constants),a.connect()):(a=new C(r,this,this.constants),this.edges[o]=a)}this.moving=!0,this._updateValueRange(e)},N.prototype._removeEdges=function(t){for(var e=this.edges,i=0,n=t.length;n>i;i++){var s=t[i],o=e[s];o&&(o.disconnect(),delete e[s])}this.moving=!0,this._updateValueRange(e)},N.prototype._reconnectEdges=function(){var t,e=this.nodes,i=this.edges;for(t in e)e.hasOwnProperty(t)&&(e[t].edges=[]);for(t in i)if(i.hasOwnProperty(t)){var n=i[t];n.from=null,n.to=null,n.connect()}},N.prototype._updateValueRange=function(t){var e,i=void 0,n=void 0;for(e in t)if(t.hasOwnProperty(e)){var s=t[e].getValue();void 0!==s&&(i=void 0===i?s:Math.min(s,i),n=void 0===n?s:Math.max(s,n))}if(void 0!==i&&void 0!==n)for(e in t)t.hasOwnProperty(e)&&t[e].setValueRange(i,n)},N.prototype.redraw=function(){this.setSize(this.width,this.height),this._redraw()},N.prototype._redraw=function(){var t=this.frame.canvas.getContext("2d"),e=this.frame.canvas.width,i=this.frame.canvas.height;t.clearRect(0,0,e,i),t.save(),t.translate(this.translation.x,this.translation.y),t.scale(this.scale,this.scale),this._drawEdges(t),this._drawNodes(t),t.restore()},N.prototype._setTranslation=function(t,e){void 0===this.translation&&(this.translation={x:0,y:0}),void 0!==t&&(this.translation.x=t),void 0!==e&&(this.translation.y=e)},N.prototype._getTranslation=function(){return{x:this.translation.x,y:this.translation.y}},N.prototype._setScale=function(t){this.scale=t},N.prototype._getScale=function(){return this.scale},N.prototype._canvasToX=function(t){return(t-this.translation.x)/this.scale},N.prototype._xToCanvas=function(t){return t*this.scale+this.translation.x},N.prototype._canvasToY=function(t){return(t-this.translation.y)/this.scale},N.prototype._yToCanvas=function(t){return t*this.scale+this.translation.y},N.prototype._drawNodes=function(t){var e=this.nodes,i=[];for(var n in e)e.hasOwnProperty(n)&&(e[n].isSelected()?i.push(n):e[n].draw(t));for(var s=0,o=i.length;o>s;s++)e[i[s]].draw(t)},N.prototype._drawEdges=function(t){var e=this.edges;for(var i in e)if(e.hasOwnProperty(i)){var n=e[i];n.connected&&e[i].draw(t)}},N.prototype._doStabilize=function(){new Date;for(var t=0,e=this.constants.minVelocity,i=!1;!i&&this.constants.maxIterations>t;)this._calculateForces(),this._discreteStepNodes(),i=!this._isMoving(e),t++;new Date},N.prototype._calculateForces=function(){var t,e,i,n,s,o,r,a,h,d,l,p=this.nodes,c=this.edges,u=.01,f=this.frame.canvas.clientWidth/2,m=this.frame.canvas.clientHeight/2;for(t in p)if(p.hasOwnProperty(t)){var g=p[t];e=f-g.x,i=m-g.y,n=Math.atan2(i,e),o=Math.cos(n)*u,r=Math.sin(n)*u,g._setForce(o,r)}var v=this.constants.nodes.distance,y=10;for(var b in p)if(p.hasOwnProperty(b)){var w=p[b];for(var _ in p)if(p.hasOwnProperty(_)){var E=p[_];e=E.x-w.x,i=E.y-w.y,s=Math.sqrt(e*e+i*i),n=Math.atan2(i,e),a=1/(1+Math.exp((s/v-1)*y)),o=Math.cos(n)*a,r=Math.sin(n)*a,w._addForce(-o,-r),E._addForce(o,r)}}for(t in c)if(c.hasOwnProperty(t)){var T=c[t];T.connected&&(e=T.to.x-T.from.x,i=T.to.y-T.from.y,l=T.length,d=Math.sqrt(e*e+i*i),n=Math.atan2(i,e),h=T.stiffness*(l-d),o=Math.cos(n)*h,r=Math.sin(n)*h,T.from._addForce(-o,-r),T.to._addForce(o,r))}},N.prototype._isMoving=function(t){var e=this.nodes;for(var i in e)if(e.hasOwnProperty(i)&&e[i].isMoving(t))return!0;return!1},N.prototype._discreteStepNodes=function(){var t=this.refreshRate/1e3,e=this.nodes;for(var i in e)e.hasOwnProperty(i)&&e[i].discreteStep(t)},N.prototype.start=function(){if(this.moving){this._calculateForces(),this._discreteStepNodes();var t=this.constants.minVelocity;this.moving=this._isMoving(t)}if(this.moving){if(!this.timer){var e=this;this.timer=window.setTimeout(function(){e.timer=void 0,e.start(),e._redraw()},this.refreshRate)}}else this._redraw()},N.prototype.stop=function(){this.timer&&(window.clearInterval(this.timer),this.timer=void 0)};var R={util:A,events:F,Controller:p,DataSet:o,DataView:r,Range:h,Stack:a,TimeStep:TimeStep,EventBus:s,components:{items:{Item:b,ItemBox:w,ItemPoint:_,ItemRange:E},Component:c,Panel:u,RootPanel:f,ItemSet:y,TimeAxis:m},graph:{Node:M,Edge:C,Popup:O,Groups:Groups,Images:Images},Timeline:D,Graph:N};n!==void 0&&(n=R),i!==void 0&&i.exports!==void 0&&(i.exports=R),"function"==typeof t&&t(function(){return R}),"undefined"!=typeof window&&(window.vis=R),A.loadCss("/* vis.js stylesheet */\n.vis.timeline {\n}\n\n\n.vis.timeline.rootpanel {\n position: relative;\n overflow: hidden;\n\n border: 1px solid #bfbfbf;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n\n.vis.timeline .panel {\n position: absolute;\n overflow: hidden;\n}\n\n\n.vis.timeline .groupset {\n position: absolute;\n padding: 0;\n margin: 0;\n}\n\n.vis.timeline .labels {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n\n padding: 0;\n margin: 0;\n\n border-right: 1px solid #bfbfbf;\n box-sizing: border-box;\n -moz-box-sizing: border-box;\n}\n\n.vis.timeline .labels .label-set {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n\n overflow: hidden;\n\n border-top: none;\n border-bottom: 1px solid #bfbfbf;\n}\n\n.vis.timeline .labels .label-set .label {\n position: absolute;\n left: 0;\n top: 0;\n width: 100%;\n color: #4d4d4d;\n}\n\n.vis.timeline.top .labels .label-set .label,\n.vis.timeline.top .groupset .itemset-axis {\n border-top: 1px solid #bfbfbf;\n border-bottom: none;\n}\n\n.vis.timeline.bottom .labels .label-set .label,\n.vis.timeline.bottom .groupset .itemset-axis {\n border-top: none;\n border-bottom: 1px solid #bfbfbf;\n}\n\n.vis.timeline .labels .label-set .label .inner {\n display: inline-block;\n padding: 5px;\n}\n\n\n.vis.timeline .itemset {\n position: absolute;\n padding: 0;\n margin: 0;\n overflow: hidden;\n}\n\n.vis.timeline .background {\n}\n\n.vis.timeline .foreground {\n}\n\n.vis.timeline .itemset-axis {\n position: absolute;\n}\n\n\n.vis.timeline .item {\n position: absolute;\n color: #1A1A1A;\n border-color: #97B0F8;\n background-color: #D5DDF6;\n display: inline-block;\n}\n\n.vis.timeline .item.selected {\n border-color: #FFC200;\n background-color: #FFF785;\n z-index: 999;\n}\n\n.vis.timeline .item.cluster {\n /* TODO: use another color or pattern? */\n background: #97B0F8 url('img/cluster_bg.png');\n color: white;\n}\n.vis.timeline .item.cluster.point {\n border-color: #D5DDF6;\n}\n\n.vis.timeline .item.box {\n text-align: center;\n border-style: solid;\n border-width: 1px;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.point {\n background: none;\n}\n\n.vis.timeline .dot {\n border: 5px solid #97B0F8;\n position: absolute;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.range {\n overflow: hidden;\n border-style: solid;\n border-width: 1px;\n border-radius: 2px;\n -moz-border-radius: 2px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.rangeoverflow {\n border-style: solid;\n border-width: 1px;\n border-radius: 2px;\n -moz-border-radius: 2px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.range .drag-left, .vis.timeline .item.rangeoverflow .drag-left {\n cursor: w-resize;\n z-index: 1000;\n}\n\n.vis.timeline .item.range .drag-right, .vis.timeline .item.rangeoverflow .drag-right {\n cursor: e-resize;\n z-index: 1000;\n}\n\n.vis.timeline .item.range .content, .vis.timeline .item.rangeoverflow .content {\n position: relative;\n display: inline-block;\n}\n\n.vis.timeline .item.line {\n position: absolute;\n width: 0;\n border-left-width: 1px;\n border-left-style: solid;\n}\n\n.vis.timeline .item .content {\n margin: 5px;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.vis.timeline .axis {\n position: relative;\n}\n\n.vis.timeline .axis .text {\n position: absolute;\n color: #4d4d4d;\n padding: 3px;\n white-space: nowrap;\n}\n\n.vis.timeline .axis .text.measure {\n position: absolute;\n padding-left: 0;\n padding-right: 0;\n margin-left: 0;\n margin-right: 0;\n visibility: hidden;\n}\n\n.vis.timeline .axis .grid.vertical {\n position: absolute;\n width: 0;\n border-right: 1px solid;\n}\n\n.vis.timeline .axis .grid.horizontal {\n position: absolute;\n left: 0;\n width: 100%;\n height: 0;\n border-bottom: 1px solid;\n}\n\n.vis.timeline .axis .grid.minor {\n border-color: #e5e5e5;\n}\n\n.vis.timeline .axis .grid.major {\n border-color: #bfbfbf;\n}\n\n.vis.timeline .currenttime {\n background-color: #FF7F6E;\n width: 2px;\n z-index: 9;\n}\n.vis.timeline .customtime {\n background-color: #6E94FF;\n width: 2px;\n cursor: move;\n z-index: 9;\n}\n")},{hammerjs:1,moment:2}]},{},[3])(3)}); \ No newline at end of file