| @ -0,0 +1,2 @@ | |||||
| .idea | |||||
| node_modules | |||||
| @ -0,0 +1,114 @@ | |||||
| /** | |||||
| * Jake build script | |||||
| */ | |||||
| var jake = require('jake'), | |||||
| fs = require('fs'), | |||||
| path = require('path'); | |||||
| require('jake-utils'); | |||||
| /** | |||||
| * default task | |||||
| */ | |||||
| desc('Execute all tasks: build all libraries'); | |||||
| task('default', ['timeline'], function () { | |||||
| console.log('done'); | |||||
| }); | |||||
| /** | |||||
| * timeline | |||||
| */ | |||||
| desc('Build the timeline visualization'); | |||||
| task('timeline', function () { | |||||
| var TIMELINE = './bin/timeline/timeline.js'; | |||||
| var TIMELINE_MIN = './bin/timeline/timeline.min.js'; | |||||
| var DIR = './bin/timeline'; | |||||
| jake.rmRf(DIR); | |||||
| jake.mkdirP(DIR); | |||||
| // concatenate the script files | |||||
| concat({ | |||||
| dest: TIMELINE, | |||||
| src: [ | |||||
| './src/header.js', | |||||
| './src/util.js', | |||||
| './src/events.js', | |||||
| './src/timestep.js', | |||||
| './src/dataset.js', | |||||
| './src/stack.js', | |||||
| './src/range.js', | |||||
| './src/controller.js', | |||||
| './src/component/component.js', | |||||
| './src/component/panel.js', | |||||
| './src/component/rootpanel.js', | |||||
| './src/component/timeaxis.js', | |||||
| './src/component/itemset.js', | |||||
| './src/component/item/*.js', | |||||
| './src/visualization/timeline.js', | |||||
| './lib/moment.js' | |||||
| ], | |||||
| separator: '\n' | |||||
| }); | |||||
| // concatenate the css files | |||||
| concat({ | |||||
| dest: './bin/timeline/timeline.css', | |||||
| src: [ | |||||
| './src/component/css/panel.css', | |||||
| './src/component/css/item.css', | |||||
| './src/component/css/timeaxis.css' | |||||
| ], | |||||
| separator: '\n' | |||||
| }); | |||||
| // minify javascript | |||||
| minify({ | |||||
| src: TIMELINE, | |||||
| dest: TIMELINE_MIN, | |||||
| header: read('./src/header.js') | |||||
| }); | |||||
| // update version number and stuff in the javascript files | |||||
| [TIMELINE, TIMELINE_MIN].forEach(function (file) { | |||||
| replace({ | |||||
| replacements: [ | |||||
| {pattern: '@@name', replacement: 'timeline'}, | |||||
| {pattern: '@@date', replacement: today()}, | |||||
| {pattern: '@@version', replacement: version()} | |||||
| ], | |||||
| src: file | |||||
| }); | |||||
| }); | |||||
| // copy examples | |||||
| jake.cpR('./src/examples/timeline', './bin/timeline/examples/'); | |||||
| console.log('created timeline library'); | |||||
| }); | |||||
| /** | |||||
| * Recursively remove a directory and its files | |||||
| * https://gist.github.com/tkihira/2367067 | |||||
| * @param {String} dir | |||||
| */ | |||||
| var rmdir = function(dir) { | |||||
| var list = fs.readdirSync(dir); | |||||
| for(var i = 0; i < list.length; i++) { | |||||
| var filename = path.join(dir, list[i]); | |||||
| var stat = fs.statSync(filename); | |||||
| if(filename == "." || filename == "..") { | |||||
| // pass these files | |||||
| } else if(stat.isDirectory()) { | |||||
| // rmdir recursively | |||||
| rmdir(filename); | |||||
| } else { | |||||
| // rm fiilename | |||||
| fs.unlinkSync(filename); | |||||
| } | |||||
| } | |||||
| fs.rmdirSync(dir); | |||||
| }; | |||||
| @ -0,0 +1,176 @@ | |||||
| Apache License | |||||
| Version 2.0, January 2004 | |||||
| http://www.apache.org/licenses/ | |||||
| TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |||||
| 1. Definitions. | |||||
| "License" shall mean the terms and conditions for use, reproduction, | |||||
| and distribution as defined by Sections 1 through 9 of this document. | |||||
| "Licensor" shall mean the copyright owner or entity authorized by | |||||
| the copyright owner that is granting the License. | |||||
| "Legal Entity" shall mean the union of the acting entity and all | |||||
| other entities that control, are controlled by, or are under common | |||||
| control with that entity. For the purposes of this definition, | |||||
| "control" means (i) the power, direct or indirect, to cause the | |||||
| direction or management of such entity, whether by contract or | |||||
| otherwise, or (ii) ownership of fifty percent (50%) or more of the | |||||
| outstanding shares, or (iii) beneficial ownership of such entity. | |||||
| "You" (or "Your") shall mean an individual or Legal Entity | |||||
| exercising permissions granted by this License. | |||||
| "Source" form shall mean the preferred form for making modifications, | |||||
| including but not limited to software source code, documentation | |||||
| source, and configuration files. | |||||
| "Object" form shall mean any form resulting from mechanical | |||||
| transformation or translation of a Source form, including but | |||||
| not limited to compiled object code, generated documentation, | |||||
| and conversions to other media types. | |||||
| "Work" shall mean the work of authorship, whether in Source or | |||||
| Object form, made available under the License, as indicated by a | |||||
| copyright notice that is included in or attached to the work | |||||
| (an example is provided in the Appendix below). | |||||
| "Derivative Works" shall mean any work, whether in Source or Object | |||||
| form, that is based on (or derived from) the Work and for which the | |||||
| editorial revisions, annotations, elaborations, or other modifications | |||||
| represent, as a whole, an original work of authorship. For the purposes | |||||
| of this License, Derivative Works shall not include works that remain | |||||
| separable from, or merely link (or bind by name) to the interfaces of, | |||||
| the Work and Derivative Works thereof. | |||||
| "Contribution" shall mean any work of authorship, including | |||||
| the original version of the Work and any modifications or additions | |||||
| to that Work or Derivative Works thereof, that is intentionally | |||||
| submitted to Licensor for inclusion in the Work by the copyright owner | |||||
| or by an individual or Legal Entity authorized to submit on behalf of | |||||
| the copyright owner. For the purposes of this definition, "submitted" | |||||
| means any form of electronic, verbal, or written communication sent | |||||
| to the Licensor or its representatives, including but not limited to | |||||
| communication on electronic mailing lists, source code control systems, | |||||
| and issue tracking systems that are managed by, or on behalf of, the | |||||
| Licensor for the purpose of discussing and improving the Work, but | |||||
| excluding communication that is conspicuously marked or otherwise | |||||
| designated in writing by the copyright owner as "Not a Contribution." | |||||
| "Contributor" shall mean Licensor and any individual or Legal Entity | |||||
| on behalf of whom a Contribution has been received by Licensor and | |||||
| subsequently incorporated within the Work. | |||||
| 2. Grant of Copyright License. Subject to the terms and conditions of | |||||
| this License, each Contributor hereby grants to You a perpetual, | |||||
| worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |||||
| copyright license to reproduce, prepare Derivative Works of, | |||||
| publicly display, publicly perform, sublicense, and distribute the | |||||
| Work and such Derivative Works in Source or Object form. | |||||
| 3. Grant of Patent License. Subject to the terms and conditions of | |||||
| this License, each Contributor hereby grants to You a perpetual, | |||||
| worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |||||
| (except as stated in this section) patent license to make, have made, | |||||
| use, offer to sell, sell, import, and otherwise transfer the Work, | |||||
| where such license applies only to those patent claims licensable | |||||
| by such Contributor that are necessarily infringed by their | |||||
| Contribution(s) alone or by combination of their Contribution(s) | |||||
| with the Work to which such Contribution(s) was submitted. If You | |||||
| institute patent litigation against any entity (including a | |||||
| cross-claim or counterclaim in a lawsuit) alleging that the Work | |||||
| or a Contribution incorporated within the Work constitutes direct | |||||
| or contributory patent infringement, then any patent licenses | |||||
| granted to You under this License for that Work shall terminate | |||||
| as of the date such litigation is filed. | |||||
| 4. Redistribution. You may reproduce and distribute copies of the | |||||
| Work or Derivative Works thereof in any medium, with or without | |||||
| modifications, and in Source or Object form, provided that You | |||||
| meet the following conditions: | |||||
| (a) You must give any other recipients of the Work or | |||||
| Derivative Works a copy of this License; and | |||||
| (b) You must cause any modified files to carry prominent notices | |||||
| stating that You changed the files; and | |||||
| (c) You must retain, in the Source form of any Derivative Works | |||||
| that You distribute, all copyright, patent, trademark, and | |||||
| attribution notices from the Source form of the Work, | |||||
| excluding those notices that do not pertain to any part of | |||||
| the Derivative Works; and | |||||
| (d) If the Work includes a "NOTICE" text file as part of its | |||||
| distribution, then any Derivative Works that You distribute must | |||||
| include a readable copy of the attribution notices contained | |||||
| within such NOTICE file, excluding those notices that do not | |||||
| pertain to any part of the Derivative Works, in at least one | |||||
| of the following places: within a NOTICE text file distributed | |||||
| as part of the Derivative Works; within the Source form or | |||||
| documentation, if provided along with the Derivative Works; or, | |||||
| within a display generated by the Derivative Works, if and | |||||
| wherever such third-party notices normally appear. The contents | |||||
| of the NOTICE file are for informational purposes only and | |||||
| do not modify the License. You may add Your own attribution | |||||
| notices within Derivative Works that You distribute, alongside | |||||
| or as an addendum to the NOTICE text from the Work, provided | |||||
| that such additional attribution notices cannot be construed | |||||
| as modifying the License. | |||||
| You may add Your own copyright statement to Your modifications and | |||||
| may provide additional or different license terms and conditions | |||||
| for use, reproduction, or distribution of Your modifications, or | |||||
| for any such Derivative Works as a whole, provided Your use, | |||||
| reproduction, and distribution of the Work otherwise complies with | |||||
| the conditions stated in this License. | |||||
| 5. Submission of Contributions. Unless You explicitly state otherwise, | |||||
| any Contribution intentionally submitted for inclusion in the Work | |||||
| by You to the Licensor shall be under the terms and conditions of | |||||
| this License, without any additional terms or conditions. | |||||
| Notwithstanding the above, nothing herein shall supersede or modify | |||||
| the terms of any separate license agreement you may have executed | |||||
| with Licensor regarding such Contributions. | |||||
| 6. Trademarks. This License does not grant permission to use the trade | |||||
| names, trademarks, service marks, or product names of the Licensor, | |||||
| except as required for reasonable and customary use in describing the | |||||
| origin of the Work and reproducing the content of the NOTICE file. | |||||
| 7. Disclaimer of Warranty. Unless required by applicable law or | |||||
| agreed to in writing, Licensor provides the Work (and each | |||||
| Contributor provides its Contributions) on an "AS IS" BASIS, | |||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
| implied, including, without limitation, any warranties or conditions | |||||
| of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |||||
| PARTICULAR PURPOSE. You are solely responsible for determining the | |||||
| appropriateness of using or redistributing the Work and assume any | |||||
| risks associated with Your exercise of permissions under this License. | |||||
| 8. Limitation of Liability. In no event and under no legal theory, | |||||
| whether in tort (including negligence), contract, or otherwise, | |||||
| unless required by applicable law (such as deliberate and grossly | |||||
| negligent acts) or agreed to in writing, shall any Contributor be | |||||
| liable to You for damages, including any direct, indirect, special, | |||||
| incidental, or consequential damages of any character arising as a | |||||
| result of this License or out of the use or inability to use the | |||||
| Work (including but not limited to damages for loss of goodwill, | |||||
| work stoppage, computer failure or malfunction, or any and all | |||||
| other commercial damages or losses), even if such Contributor | |||||
| has been advised of the possibility of such damages. | |||||
| 9. Accepting Warranty or Additional Liability. While redistributing | |||||
| the Work or Derivative Works thereof, You may choose to offer, | |||||
| and charge a fee for, acceptance of support, warranty, indemnity, | |||||
| or other liability obligations and/or rights consistent with this | |||||
| License. However, in accepting such obligations, You may act only | |||||
| on Your own behalf and on Your sole responsibility, not on behalf | |||||
| of any other Contributor, and only if You agree to indemnify, | |||||
| defend, and hold each Contributor harmless for any liability | |||||
| incurred by, or claims asserted against, such Contributor by reason | |||||
| of your accepting any such warranty or additional liability. | |||||
| END OF TERMS AND CONDITIONS | |||||
| @ -0,0 +1,14 @@ | |||||
| Vis.js | |||||
| Copyright 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 | |||||
| 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. | |||||
| @ -1,4 +1,10 @@ | |||||
| vis | |||||
| === | |||||
| vis.js | |||||
| ================== | |||||
| Dynamic, browser based visualization library | |||||
| Vis.js is a dynamic, browser based visualization library. | |||||
| The library is designed to be easy to use, to handle large amounts | |||||
| of dynamic data, and to enable manipulation of the data. | |||||
| The library consists of Timeline, LineChart, LineChart3d, Graph, and Treegrid. | |||||
| vis.js Library is part of [CHAP](http://chap.almende.com), | |||||
| the Common Hybrid Agent Platform, developed by [Almende B.V](http://almende.com). | |||||
| @ -0,0 +1,62 @@ | |||||
| <!DOCTYPE HTML> | |||||
| <html> | |||||
| <head> | |||||
| <title>Timeline demo</title> | |||||
| <script src="../timeline.js"></script> | |||||
| <link href="../timeline.css" rel="stylesheet" type="text/css" /> | |||||
| <style> | |||||
| body, html { | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| padding: 0; | |||||
| margin: 0; | |||||
| font-family: arial, sans-serif; | |||||
| font-size: 12pt; | |||||
| } | |||||
| #visualization { | |||||
| box-sizing: border-box; | |||||
| padding: 10px; | |||||
| width: 100%; | |||||
| height: 300px; | |||||
| } | |||||
| #visualization .itemset { | |||||
| /*background: rgba(255, 255, 0, 0.5);*/ | |||||
| } | |||||
| </style> | |||||
| </head> | |||||
| <body> | |||||
| <div id="visualization"></div> | |||||
| <script> | |||||
| // create a dataset with items | |||||
| var now = moment().minutes(0).seconds(0).milliseconds(0); | |||||
| var data = new DataSet({ | |||||
| fieldTypes: { | |||||
| start: 'Date', | |||||
| end: 'Date' | |||||
| } | |||||
| }); | |||||
| data.add([ | |||||
| {id: 1, content: 'item 1<br>start', start: now.clone().add('days', 4).toDate()}, | |||||
| {id: 2, content: 'item 2', start: now.clone().add('days', -2).toDate() }, | |||||
| {id: 3, content: 'item 3', start: now.clone().add('days', 2).toDate()}, | |||||
| {id: 4, content: 'item 4', start: now.clone().add('days', 0).toDate(), end: now.clone().add('days', 3).toDate()}, | |||||
| {id: 5, content: 'item 5', start: now.clone().add('days', 9).toDate(), type:'point'}, | |||||
| {id: 6, content: 'item 6', start: now.clone().add('days', 11).toDate()} | |||||
| ]); | |||||
| var container = document.getElementById('visualization'); | |||||
| var options = { | |||||
| start: now.clone().add('days', -3).valueOf(), | |||||
| end: now.clone().add('days', 7).valueOf() | |||||
| }; | |||||
| var timeline = new Timeline(container, data, options); | |||||
| </script> | |||||
| </body> | |||||
| </html> | |||||
| @ -0,0 +1,142 @@ | |||||
| .graph { | |||||
| position: relative; | |||||
| border: 1px solid #bfbfbf; | |||||
| } | |||||
| .graph .panel { | |||||
| position: absolute; | |||||
| } | |||||
| .graph .itemset { | |||||
| position: absolute; | |||||
| } | |||||
| .graph .item { | |||||
| position: absolute; | |||||
| color: #1A1A1A; | |||||
| border-color: #97B0F8; | |||||
| background-color: #D5DDF6; | |||||
| display: inline-block; | |||||
| } | |||||
| .graph .item.selected { | |||||
| border-color: #FFC200; | |||||
| background-color: #FFF785; | |||||
| z-index: 999; | |||||
| } | |||||
| .graph .item.cluster { | |||||
| /* TODO: use another color or pattern? */ | |||||
| background: #97B0F8 url('img/cluster_bg.png'); | |||||
| color: white; | |||||
| } | |||||
| .graph .item.cluster.point { | |||||
| border-color: #D5DDF6; | |||||
| } | |||||
| .graph .item.box { | |||||
| text-align: center; | |||||
| border-style: solid; | |||||
| border-width: 1px; | |||||
| border-radius: 5px; | |||||
| -moz-border-radius: 5px; /* For Firefox 3.6 and older */ | |||||
| } | |||||
| .graph .item.point { | |||||
| background: none; | |||||
| } | |||||
| .graph .dot { | |||||
| border: 5px solid #97B0F8; | |||||
| position: absolute; | |||||
| border-radius: 5px; | |||||
| -moz-border-radius: 5px; /* For Firefox 3.6 and older */ | |||||
| } | |||||
| .graph .item.range { | |||||
| overflow: hidden; | |||||
| border-style: solid; | |||||
| border-width: 1px; | |||||
| border-radius: 2px; | |||||
| -moz-border-radius: 2px; /* For Firefox 3.6 and older */ | |||||
| } | |||||
| .graph .item.range .drag-left { | |||||
| cursor: w-resize; | |||||
| z-index: 1000; | |||||
| } | |||||
| .graph .item.range .drag-right { | |||||
| cursor: e-resize; | |||||
| z-index: 1000; | |||||
| } | |||||
| .graph .item.range .content { | |||||
| position: relative; | |||||
| display: inline-block; | |||||
| } | |||||
| .graph .item.line { | |||||
| position: absolute; | |||||
| width: 0; | |||||
| border-left-width: 1px; | |||||
| border-left-style: solid; | |||||
| z-index: -1; | |||||
| } | |||||
| .graph .item .content { | |||||
| margin: 5px; | |||||
| white-space: nowrap; | |||||
| overflow: hidden; | |||||
| } | |||||
| /* TODO: better css name, 'graph' is way to generic */ | |||||
| .graph { | |||||
| overflow: hidden; | |||||
| } | |||||
| .graph .axis { | |||||
| position: relative; | |||||
| } | |||||
| .graph .axis .text { | |||||
| position: absolute; | |||||
| color: #4d4d4d; | |||||
| padding: 3px; | |||||
| white-space: nowrap; | |||||
| } | |||||
| .graph .axis .text.measure { | |||||
| position: absolute; | |||||
| padding-left: 0; | |||||
| padding-right: 0; | |||||
| margin-left: 0; | |||||
| margin-right: 0; | |||||
| visibility: hidden; | |||||
| } | |||||
| .graph .axis .grid.vertical { | |||||
| position: absolute; | |||||
| width: 0; | |||||
| border-right: 1px solid; | |||||
| } | |||||
| .graph .axis .grid.horizontal { | |||||
| position: absolute; | |||||
| left: 0; | |||||
| width: 100%; | |||||
| height: 0; | |||||
| border-bottom: 1px solid; | |||||
| } | |||||
| .graph .axis .grid.minor { | |||||
| border-color: #e5e5e5; | |||||
| } | |||||
| .graph .axis .grid.major { | |||||
| border-color: #bfbfbf; | |||||
| } | |||||
| @ -0,0 +1,30 @@ | |||||
| { | |||||
| "name": "vis", | |||||
| "version": "3.0.0-SNAPSHOT", | |||||
| "description": "A dynamic, browser-based visualization library.", | |||||
| "homepage": "https://github.com/almende/vis", | |||||
| "repository": { | |||||
| "type": "git", | |||||
| "url": "git://github.com/almende/vis.git" | |||||
| }, | |||||
| "keywords": [ | |||||
| "visualization", | |||||
| "web based", | |||||
| "browser based", | |||||
| "chart", | |||||
| "linechart", | |||||
| "timeline", | |||||
| "graph", | |||||
| "network", | |||||
| "javascript", | |||||
| "browser" | |||||
| ], | |||||
| "dependencies": {}, | |||||
| "devDependencies": { | |||||
| "jake": ">= 0.5.9", | |||||
| "jake-utils": ">= 0.0.18", | |||||
| "uglify-js": ">= 2.2.5", | |||||
| "nodeunit": ">= 0.7.4", | |||||
| "dateable": ">= 0.1.2" | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,116 @@ | |||||
| /** | |||||
| * 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; | |||||
| } | |||||
| /** | |||||
| * Set parameters for the frame. Parameters will be merged in current parameter | |||||
| * set. | |||||
| * @param {Object} options Available parameters: | |||||
| * {String | function} [className] | |||||
| * {String | Number | function} [left] | |||||
| * {String | Number | function} [top] | |||||
| * {String | Number | function} [width] | |||||
| * {String | Number | function} [height] | |||||
| */ | |||||
| Component.prototype.setOptions = function(options) { | |||||
| if (options) { | |||||
| util.extend(this.options, options); | |||||
| } | |||||
| if (this.controller) { | |||||
| this.requestRepaint(); | |||||
| this.requestReflow(); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Get the container element of the component, which can be used by a child to | |||||
| * add its own widgets. Not all components do have a container for childs, in | |||||
| * that case null is returned. | |||||
| * @returns {HTMLElement | null} container | |||||
| */ | |||||
| Component.prototype.getContainer = function () { | |||||
| // should be implemented by the component | |||||
| return null; | |||||
| }; | |||||
| /** | |||||
| * Get the frame element of the component, the outer HTML DOM element. | |||||
| * @returns {HTMLElement | null} frame | |||||
| */ | |||||
| Component.prototype.getFrame = function () { | |||||
| return this.frame; | |||||
| }; | |||||
| /** | |||||
| * Repaint the component | |||||
| * @return {Boolean} changed | |||||
| */ | |||||
| Component.prototype.repaint = function () { | |||||
| // should be implemented by the component | |||||
| return false; | |||||
| }; | |||||
| /** | |||||
| * Reflow the component | |||||
| * @return {Boolean} resized | |||||
| */ | |||||
| Component.prototype.reflow = function () { | |||||
| // should be implemented by the component | |||||
| return false; | |||||
| }; | |||||
| /** | |||||
| * Request a repaint. The controller will schedule a repaint | |||||
| */ | |||||
| Component.prototype.requestRepaint = function () { | |||||
| 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 () { | |||||
| 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? | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Event handler | |||||
| * @param {String} event name of the event, for example 'click', 'mousemove' | |||||
| * @param {function} callback callback handler, invoked with the raw HTML Event | |||||
| * as parameter. | |||||
| */ | |||||
| Component.prototype.on = function (event, callback) { | |||||
| // TODO: rethink the way of event delegation | |||||
| if (this.parent) { | |||||
| this.parent.on(event, callback); | |||||
| } | |||||
| else { | |||||
| throw new Error('Cannot attach event: no root panel found'); | |||||
| } | |||||
| }; | |||||
| @ -0,0 +1,84 @@ | |||||
| .graph .itemset { | |||||
| position: absolute; | |||||
| } | |||||
| .graph .item { | |||||
| position: absolute; | |||||
| color: #1A1A1A; | |||||
| border-color: #97B0F8; | |||||
| background-color: #D5DDF6; | |||||
| display: inline-block; | |||||
| } | |||||
| .graph .item.selected { | |||||
| border-color: #FFC200; | |||||
| background-color: #FFF785; | |||||
| z-index: 999; | |||||
| } | |||||
| .graph .item.cluster { | |||||
| /* TODO: use another color or pattern? */ | |||||
| background: #97B0F8 url('img/cluster_bg.png'); | |||||
| color: white; | |||||
| } | |||||
| .graph .item.cluster.point { | |||||
| border-color: #D5DDF6; | |||||
| } | |||||
| .graph .item.box { | |||||
| text-align: center; | |||||
| border-style: solid; | |||||
| border-width: 1px; | |||||
| border-radius: 5px; | |||||
| -moz-border-radius: 5px; /* For Firefox 3.6 and older */ | |||||
| } | |||||
| .graph .item.point { | |||||
| background: none; | |||||
| } | |||||
| .graph .dot { | |||||
| border: 5px solid #97B0F8; | |||||
| position: absolute; | |||||
| border-radius: 5px; | |||||
| -moz-border-radius: 5px; /* For Firefox 3.6 and older */ | |||||
| } | |||||
| .graph .item.range { | |||||
| overflow: hidden; | |||||
| border-style: solid; | |||||
| border-width: 1px; | |||||
| border-radius: 2px; | |||||
| -moz-border-radius: 2px; /* For Firefox 3.6 and older */ | |||||
| } | |||||
| .graph .item.range .drag-left { | |||||
| cursor: w-resize; | |||||
| z-index: 1000; | |||||
| } | |||||
| .graph .item.range .drag-right { | |||||
| cursor: e-resize; | |||||
| z-index: 1000; | |||||
| } | |||||
| .graph .item.range .content { | |||||
| position: relative; | |||||
| display: inline-block; | |||||
| } | |||||
| .graph .item.line { | |||||
| position: absolute; | |||||
| width: 0; | |||||
| border-left-width: 1px; | |||||
| border-left-style: solid; | |||||
| z-index: -1; | |||||
| } | |||||
| .graph .item .content { | |||||
| margin: 5px; | |||||
| white-space: nowrap; | |||||
| overflow: hidden; | |||||
| } | |||||
| @ -0,0 +1,9 @@ | |||||
| .graph { | |||||
| position: relative; | |||||
| border: 1px solid #bfbfbf; | |||||
| } | |||||
| .graph .panel { | |||||
| position: absolute; | |||||
| } | |||||
| @ -0,0 +1,47 @@ | |||||
| /* TODO: better css name, 'graph' is way to generic */ | |||||
| .graph { | |||||
| overflow: hidden; | |||||
| } | |||||
| .graph .axis { | |||||
| position: relative; | |||||
| } | |||||
| .graph .axis .text { | |||||
| position: absolute; | |||||
| color: #4d4d4d; | |||||
| padding: 3px; | |||||
| white-space: nowrap; | |||||
| } | |||||
| .graph .axis .text.measure { | |||||
| position: absolute; | |||||
| padding-left: 0; | |||||
| padding-right: 0; | |||||
| margin-left: 0; | |||||
| margin-right: 0; | |||||
| visibility: hidden; | |||||
| } | |||||
| .graph .axis .grid.vertical { | |||||
| position: absolute; | |||||
| width: 0; | |||||
| border-right: 1px solid; | |||||
| } | |||||
| .graph .axis .grid.horizontal { | |||||
| position: absolute; | |||||
| left: 0; | |||||
| width: 100%; | |||||
| height: 0; | |||||
| border-bottom: 1px solid; | |||||
| } | |||||
| .graph .axis .grid.minor { | |||||
| border-color: #e5e5e5; | |||||
| } | |||||
| .graph .axis .grid.major { | |||||
| border-color: #bfbfbf; | |||||
| } | |||||
| @ -0,0 +1,36 @@ | |||||
| /** | |||||
| * @constructor Item | |||||
| * @param {ItemSet} parent | |||||
| * @param {Object} data Object containing (optional) parameters type, | |||||
| * start, end, content, group, className. | |||||
| * @param {Object} [options] Options to set initial property values | |||||
| * // TODO: describe available options | |||||
| */ | |||||
| function Item (parent, data, options) { | |||||
| this.parent = parent; | |||||
| this.data = data; | |||||
| this.selected = false; | |||||
| this.visible = true; | |||||
| this.dom = null; | |||||
| this.options = options; | |||||
| } | |||||
| Item.prototype = new Component(); | |||||
| /** | |||||
| * Select current item | |||||
| */ | |||||
| Item.prototype.select = function () { | |||||
| this.selected = true; | |||||
| }; | |||||
| /** | |||||
| * Unselect current item | |||||
| */ | |||||
| Item.prototype.unselect = function () { | |||||
| this.selected = false; | |||||
| }; | |||||
| // create a namespace for all item types | |||||
| var itemTypes = {}; | |||||
| @ -0,0 +1,270 @@ | |||||
| /** | |||||
| * @constructor ItemBox | |||||
| * @extends Item | |||||
| * @param {ItemSet} parent | |||||
| * @param {Object} data Object containing parameters start | |||||
| * content, className. | |||||
| * @param {Object} [options] Options to set initial property values | |||||
| * // TODO: describe available options | |||||
| */ | |||||
| function ItemBox (parent, data, options) { | |||||
| 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); | |||||
| } | |||||
| ItemBox.prototype = new Item (null, null); | |||||
| // register the ItemBox in the item types | |||||
| itemTypes['box'] = ItemBox; | |||||
| /** | |||||
| * Select the item | |||||
| * @override | |||||
| */ | |||||
| ItemBox.prototype.select = function () { | |||||
| this.selected = true; | |||||
| // TODO: select and unselect | |||||
| }; | |||||
| /** | |||||
| * Unselect the item | |||||
| * @override | |||||
| */ | |||||
| ItemBox.prototype.unselect = function () { | |||||
| this.selected = false; | |||||
| // TODO: select and unselect | |||||
| }; | |||||
| /** | |||||
| * Repaint the item | |||||
| * @return {Boolean} changed | |||||
| */ | |||||
| ItemBox.prototype.repaint = function () { | |||||
| // TODO: make an efficient repaint | |||||
| var changed = false; | |||||
| var dom = this.dom; | |||||
| if (this.visible) { | |||||
| if (!dom) { | |||||
| this._create(); | |||||
| changed = true; | |||||
| } | |||||
| dom = this.dom; | |||||
| if (dom) { | |||||
| if (!this.options && !this.parent) { | |||||
| throw new Error('Cannot repaint item: no parent attached'); | |||||
| } | |||||
| var parentContainer = this.parent.getContainer(); | |||||
| if (!parentContainer) { | |||||
| throw new Error('Cannot repaint time axis: parent has no container element'); | |||||
| } | |||||
| if (!dom.box.parentNode) { | |||||
| parentContainer.appendChild(dom.box); | |||||
| changed = true; | |||||
| } | |||||
| if (!dom.line.parentNode) { | |||||
| parentContainer.appendChild(dom.line); | |||||
| changed = true; | |||||
| } | |||||
| if (!dom.dot.parentNode) { | |||||
| parentContainer.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; | |||||
| } | |||||
| } | |||||
| } | |||||
| else { | |||||
| // hide when visible | |||||
| if (dom) { | |||||
| if (dom.box.parentNode) { | |||||
| dom.box.parentNode.removeChild(dom.box); | |||||
| changed = true; | |||||
| } | |||||
| if (dom.line.parentNode) { | |||||
| dom.line.parentNode.removeChild(dom.line); | |||||
| changed = true; | |||||
| } | |||||
| if (dom.dot.parentNode) { | |||||
| dom.dot.parentNode.removeChild(dom.dot); | |||||
| changed = true; | |||||
| } | |||||
| } | |||||
| } | |||||
| return changed; | |||||
| }; | |||||
| /** | |||||
| * Reflow the item: calculate its actual size and position from the DOM | |||||
| * @return {boolean} resized returns true if the axis is resized | |||||
| * @override | |||||
| */ | |||||
| ItemBox.prototype.reflow = function () { | |||||
| if (this.data.start == undefined) { | |||||
| throw new Error('Property "start" missing in item ' + this.data.id); | |||||
| } | |||||
| var update = util.updateProperty, | |||||
| dom = this.dom, | |||||
| props = this.props, | |||||
| options = this.options, | |||||
| start = this.parent.toScreen(this.data.start), | |||||
| align = options && options.align, | |||||
| orientation = options.orientation, | |||||
| changed = 0, | |||||
| top, | |||||
| left; | |||||
| if (dom) { | |||||
| 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, 'width', dom.line.offsetWidth); | |||||
| 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); | |||||
| if (orientation == 'top') { | |||||
| top = options.margin.axis; | |||||
| changed += update(this, 'top', top); | |||||
| changed += update(props.line, 'top', 0); | |||||
| changed += update(props.line, 'height', top); | |||||
| changed += update(props.dot, 'top', -props.dot.height / 2); | |||||
| } | |||||
| else { | |||||
| // default or 'bottom' | |||||
| var parentHeight = this.parent.height; | |||||
| top = parentHeight - this.height - options.margin.axis; | |||||
| changed += update(this, 'top', top); | |||||
| changed += update(props.line, 'top', top + this.height); | |||||
| changed += update(props.line, 'height', Math.max(options.margin.axis, 0)); | |||||
| changed += update(props.dot, 'top', parentHeight - props.dot.height / 2); | |||||
| } | |||||
| } | |||||
| else { | |||||
| changed += 1; | |||||
| } | |||||
| return (changed > 0); | |||||
| }; | |||||
| /** | |||||
| * Create an items DOM | |||||
| * @private | |||||
| */ | |||||
| ItemBox.prototype._create = function () { | |||||
| 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'; | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Reposition the item, recalculate its left, top, and width, using the current | |||||
| * range and size of the items itemset | |||||
| * @override | |||||
| */ | |||||
| ItemBox.prototype.reposition = function () { | |||||
| var dom = this.dom, | |||||
| props = this.props, | |||||
| orientation = this.options.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 = props.line.top + 'px'; | |||||
| line.style.top = (this.top + this.height) + 'px'; | |||||
| line.style.height = (props.dot.top - this.top - this.height) + 'px'; | |||||
| } | |||||
| dot.style.left = props.dot.left + 'px'; | |||||
| dot.style.top = props.dot.top + 'px'; | |||||
| } | |||||
| }; | |||||
| @ -0,0 +1,209 @@ | |||||
| /** | |||||
| * @constructor ItemPoint | |||||
| * @extends Item | |||||
| * @param {ItemSet} parent | |||||
| * @param {Object} data Object containing parameters start | |||||
| * content, className. | |||||
| * @param {Object} [options] Options to set initial property values | |||||
| * // TODO: describe available options | |||||
| */ | |||||
| function ItemPoint (parent, data, options) { | |||||
| this.props = { | |||||
| dot: { | |||||
| top: 0, | |||||
| width: 0, | |||||
| height: 0 | |||||
| }, | |||||
| content: { | |||||
| height: 0, | |||||
| marginLeft: 0 | |||||
| } | |||||
| }; | |||||
| Item.call(this, parent, data, options); | |||||
| } | |||||
| ItemPoint.prototype = new Item (null, null); | |||||
| // register the ItemPoint in the item types | |||||
| itemTypes['point'] = ItemPoint; | |||||
| /** | |||||
| * Select the item | |||||
| * @override | |||||
| */ | |||||
| ItemPoint.prototype.select = function () { | |||||
| this.selected = true; | |||||
| // TODO: select and unselect | |||||
| }; | |||||
| /** | |||||
| * Unselect the item | |||||
| * @override | |||||
| */ | |||||
| ItemPoint.prototype.unselect = function () { | |||||
| this.selected = false; | |||||
| // TODO: select and unselect | |||||
| }; | |||||
| /** | |||||
| * Repaint the item | |||||
| * @return {Boolean} changed | |||||
| */ | |||||
| ItemPoint.prototype.repaint = function () { | |||||
| // TODO: make an efficient repaint | |||||
| var changed = false; | |||||
| var dom = this.dom; | |||||
| if (this.visible) { | |||||
| if (!dom) { | |||||
| this._create(); | |||||
| changed = true; | |||||
| } | |||||
| dom = this.dom; | |||||
| if (dom) { | |||||
| if (!this.options && !this.options.parent) { | |||||
| throw new Error('Cannot repaint item: no parent attached'); | |||||
| } | |||||
| var parentContainer = this.parent.getContainer(); | |||||
| if (!parentContainer) { | |||||
| throw new Error('Cannot repaint time axis: parent has no container element'); | |||||
| } | |||||
| if (!dom.point.parentNode) { | |||||
| parentContainer.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; | |||||
| } | |||||
| } | |||||
| } | |||||
| else { | |||||
| // hide when visible | |||||
| if (dom) { | |||||
| if (dom.point.parentNode) { | |||||
| dom.point.parentNode.removeChild(dom.point); | |||||
| changed = true; | |||||
| } | |||||
| } | |||||
| } | |||||
| return changed; | |||||
| }; | |||||
| /** | |||||
| * Reflow the item: calculate its actual size from the DOM | |||||
| * @return {boolean} resized returns true if the axis is resized | |||||
| * @override | |||||
| */ | |||||
| ItemPoint.prototype.reflow = function () { | |||||
| if (this.data.start == undefined) { | |||||
| throw new Error('Property "start" missing in item ' + this.data.id); | |||||
| } | |||||
| var update = util.updateProperty, | |||||
| dom = this.dom, | |||||
| props = this.props, | |||||
| options = this.options, | |||||
| orientation = options.orientation, | |||||
| start = this.parent.toScreen(this.data.start), | |||||
| changed = 0, | |||||
| top; | |||||
| if (dom) { | |||||
| 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 = options.margin.axis; | |||||
| } | |||||
| else { | |||||
| // default or 'bottom' | |||||
| var parentHeight = this.parent.height; | |||||
| top = parentHeight - this.height - options.margin.axis; | |||||
| } | |||||
| 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); | |||||
| }; | |||||
| /** | |||||
| * Create an items DOM | |||||
| * @private | |||||
| */ | |||||
| ItemPoint.prototype._create = function () { | |||||
| 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); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Reposition the item, recalculate its left, top, and width, using the current | |||||
| * range and size of the items itemset | |||||
| * @override | |||||
| */ | |||||
| ItemPoint.prototype.reposition = function () { | |||||
| var dom = this.dom, | |||||
| props = this.props; | |||||
| 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.dot.style.top = props.dot.top + 'px'; | |||||
| } | |||||
| }; | |||||
| @ -0,0 +1,219 @@ | |||||
| /** | |||||
| * @constructor ItemRange | |||||
| * @extends Item | |||||
| * @param {ItemSet} parent | |||||
| * @param {Object} data Object containing parameters start, end | |||||
| * content, className. | |||||
| * @param {Object} [options] Options to set initial property values | |||||
| * // TODO: describe available options | |||||
| */ | |||||
| function ItemRange (parent, data, options) { | |||||
| this.props = { | |||||
| content: { | |||||
| left: 0, | |||||
| width: 0 | |||||
| } | |||||
| }; | |||||
| Item.call(this, parent, data, options); | |||||
| } | |||||
| ItemRange.prototype = new Item (null, null); | |||||
| // register the ItemBox in the item types | |||||
| itemTypes['range'] = ItemRange; | |||||
| /** | |||||
| * Select the item | |||||
| * @override | |||||
| */ | |||||
| ItemRange.prototype.select = function () { | |||||
| this.selected = true; | |||||
| // TODO: select and unselect | |||||
| }; | |||||
| /** | |||||
| * Unselect the item | |||||
| * @override | |||||
| */ | |||||
| ItemRange.prototype.unselect = function () { | |||||
| this.selected = false; | |||||
| // TODO: select and unselect | |||||
| }; | |||||
| /** | |||||
| * Repaint the item | |||||
| * @return {Boolean} changed | |||||
| */ | |||||
| ItemRange.prototype.repaint = function () { | |||||
| // TODO: make an efficient repaint | |||||
| var changed = false; | |||||
| var dom = this.dom; | |||||
| if (this.visible) { | |||||
| if (!dom) { | |||||
| this._create(); | |||||
| changed = true; | |||||
| } | |||||
| dom = this.dom; | |||||
| if (dom) { | |||||
| if (!this.options && !this.options.parent) { | |||||
| throw new Error('Cannot repaint item: no parent attached'); | |||||
| } | |||||
| var parentContainer = this.parent.getContainer(); | |||||
| if (!parentContainer) { | |||||
| throw new Error('Cannot repaint time axis: parent has no container element'); | |||||
| } | |||||
| if (!dom.box.parentNode) { | |||||
| parentContainer.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 class | |||||
| var className = this.data.className ? ('' + this.data.className) : ''; | |||||
| if (this.className != className) { | |||||
| this.className = className; | |||||
| dom.box.className = 'item range' + className; | |||||
| changed = true; | |||||
| } | |||||
| } | |||||
| } | |||||
| else { | |||||
| // hide when visible | |||||
| if (dom) { | |||||
| if (dom.box.parentNode) { | |||||
| dom.box.parentNode.removeChild(dom.box); | |||||
| changed = true; | |||||
| } | |||||
| } | |||||
| } | |||||
| return changed; | |||||
| }; | |||||
| /** | |||||
| * Reflow the item: calculate its actual size from the DOM | |||||
| * @return {boolean} resized returns true if the axis is resized | |||||
| * @override | |||||
| */ | |||||
| ItemRange.prototype.reflow = function () { | |||||
| 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); | |||||
| } | |||||
| var dom = this.dom, | |||||
| props = this.props, | |||||
| options = this.options, | |||||
| parent = this.parent, | |||||
| start = parent.toScreen(this.data.start), | |||||
| end = parent.toScreen(this.data.end), | |||||
| changed = 0; | |||||
| if (dom) { | |||||
| var update = util.updateProperty, | |||||
| box = dom.box, | |||||
| parentWidth = parent.width, | |||||
| orientation = options.orientation, | |||||
| contentLeft, | |||||
| top; | |||||
| 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; | |||||
| } | |||||
| // 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 * options.padding)); | |||||
| // TODO: remove the need for options.padding. it's terrible. | |||||
| } | |||||
| else { | |||||
| contentLeft = 0; | |||||
| } | |||||
| changed += update(props.content, 'left', contentLeft); | |||||
| if (orientation == 'top') { | |||||
| top = options.margin.axis; | |||||
| changed += update(this, 'top', top); | |||||
| } | |||||
| else { | |||||
| // default or 'bottom' | |||||
| top = parent.height - this.height - options.margin.axis; | |||||
| 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); | |||||
| }; | |||||
| /** | |||||
| * Create an items DOM | |||||
| * @private | |||||
| */ | |||||
| ItemRange.prototype._create = function () { | |||||
| 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); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Reposition the item, recalculate its left, top, and width, using the current | |||||
| * range and size of the items itemset | |||||
| * @override | |||||
| */ | |||||
| ItemRange.prototype.reposition = function () { | |||||
| 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'; | |||||
| dom.content.style.left = props.content.left + 'px'; | |||||
| } | |||||
| }; | |||||
| @ -0,0 +1,416 @@ | |||||
| /** | |||||
| * An ItemSet holds a set of items and ranges which can be displayed in a | |||||
| * range. The width is determined by the parent of the ItemSet, and the height | |||||
| * is determined by the size of the items. | |||||
| * @param {Component} parent | |||||
| * @param {Component[]} [depends] Components on which this components depends | |||||
| * (except for the parent) | |||||
| * @param {Object} [options] See ItemSet.setOptions for the available | |||||
| * options. | |||||
| * @constructor ItemSet | |||||
| * @extends Panel | |||||
| */ | |||||
| 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 = { | |||||
| style: 'box', | |||||
| align: 'center', | |||||
| orientation: 'bottom', | |||||
| margin: { | |||||
| axis: 20, | |||||
| item: 10 | |||||
| }, | |||||
| padding: 5 | |||||
| }; | |||||
| var me = this; | |||||
| this.data = null; // DataSet | |||||
| this.range = null; // Range or Object {start: number, end: number} | |||||
| 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.items = {}; | |||||
| this.queue = {}; // queue with items to be added/updated/removed | |||||
| this.stack = new Stack(this); | |||||
| this.conversion = null; | |||||
| this.setOptions(options); | |||||
| } | |||||
| ItemSet.prototype = new Panel(); | |||||
| /** | |||||
| * Set options for the ItemSet. Existing options will be extended/overwritten. | |||||
| * @param {Object} [options] The following options are available: | |||||
| * {String | function} [className] | |||||
| * class name for the itemset | |||||
| * {String} [style] | |||||
| * Default style for the items. Choose from 'box' | |||||
| * (default), 'point', or 'range'. The default | |||||
| * Style can be overwritten by individual items. | |||||
| * {String} align | |||||
| * Alignment for the items, only applicable for | |||||
| * ItemBox. Choose 'center' (default), 'left', or | |||||
| * 'right'. | |||||
| * {String} orientation | |||||
| * Orientation of the item set. Choose 'top' or | |||||
| * 'bottom' (default). | |||||
| * {Number} margin.axis | |||||
| * Margin between the axis and the items in pixels. | |||||
| * Default is 20. | |||||
| * {Number} margin.item | |||||
| * Margin between items in pixels. Default is 10. | |||||
| * {Number} padding | |||||
| * Padding of the contents of an item in pixels. | |||||
| * Must correspond with the items css. Default is 5. | |||||
| */ | |||||
| ItemSet.prototype.setOptions = function (options) { | |||||
| util.extend(this.options, options); | |||||
| // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis | |||||
| this.stack.setOptions(this.options); | |||||
| }; | |||||
| /** | |||||
| * Set range (start and end). | |||||
| * @param {Range | Object} range A Range or an object containing start and end. | |||||
| */ | |||||
| ItemSet.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; | |||||
| }; | |||||
| /** | |||||
| * Repaint the component | |||||
| * @return {Boolean} changed | |||||
| */ | |||||
| ItemSet.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 = 'itemset'; | |||||
| if (options.className) { | |||||
| util.addClassName(frame, util.option.asString(options.className)); | |||||
| } | |||||
| this.frame = frame; | |||||
| changed += 1; | |||||
| } | |||||
| if (!frame.parentNode) { | |||||
| 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'); | |||||
| } | |||||
| parentContainer.appendChild(frame); | |||||
| changed += 1; | |||||
| } | |||||
| 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%')); | |||||
| this._updateConversion(); | |||||
| var me = this, | |||||
| queue = this.queue, | |||||
| data = this.data, | |||||
| items = this.items, | |||||
| dataOptions = { | |||||
| fields: ['id', 'start', 'end', 'content', 'type'] | |||||
| }; | |||||
| // TODO: copy options from the itemset itself? | |||||
| // TODO: make orientation dynamically changable for the items | |||||
| // show/hide added/changed/removed items | |||||
| Object.keys(queue).forEach(function (id) { | |||||
| var entry = queue[id]; | |||||
| var item = entry.item; | |||||
| //noinspection FallthroughInSwitchStatementJS | |||||
| switch (entry.action) { | |||||
| case 'add': | |||||
| case 'update': | |||||
| var itemData = data.get(id, dataOptions); | |||||
| var type = itemData.type || | |||||
| (itemData.start && itemData.end && 'range') || | |||||
| 'box'; | |||||
| var constructor = itemTypes[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, delete the item | |||||
| item.visible = false; | |||||
| changed += item.repaint(); | |||||
| item = null; | |||||
| } | |||||
| else { | |||||
| item.data = itemData; // TODO: create a method item.setData ? | |||||
| changed += item.repaint(); | |||||
| } | |||||
| } | |||||
| if (!item) { | |||||
| // create item | |||||
| if (constructor) { | |||||
| item = new constructor(me, itemData, options); | |||||
| changed += item.repaint(); | |||||
| } | |||||
| else { | |||||
| throw new TypeError('Unknown item type "' + type + '"'); | |||||
| } | |||||
| } | |||||
| // update lists | |||||
| items[id] = item; | |||||
| delete queue[id]; | |||||
| break; | |||||
| case 'remove': | |||||
| if (item) { | |||||
| // TODO: remove dom of the item | |||||
| item.visible = false; | |||||
| changed += item.repaint(); | |||||
| } | |||||
| // update lists | |||||
| delete items[id]; | |||||
| delete queue[id]; | |||||
| break; | |||||
| default: | |||||
| console.log('Error: unknown action "' + entry.action + '"'); | |||||
| } | |||||
| }); | |||||
| // reposition all items | |||||
| util.forEach(this.items, function (item) { | |||||
| item.reposition(); | |||||
| }); | |||||
| return (changed > 0); | |||||
| }; | |||||
| /** | |||||
| * Reflow the component | |||||
| * @return {Boolean} resized | |||||
| */ | |||||
| ItemSet.prototype.reflow = function () { | |||||
| var changed = 0, | |||||
| options = this.options, | |||||
| update = util.updateProperty, | |||||
| frame = this.frame; | |||||
| if (frame) { | |||||
| this._updateConversion(); | |||||
| util.forEach(this.items, function (item) { | |||||
| changed += item.reflow(); | |||||
| }); | |||||
| // 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(); | |||||
| if (options.height != null) { | |||||
| changed += update(this, 'height', frame.offsetHeight); | |||||
| } | |||||
| else { | |||||
| // height is not specified, determine the height from the height and positioned items | |||||
| var frameHeight = this.height; | |||||
| var maxHeight = 0; | |||||
| if (options.orientation == 'top') { | |||||
| util.forEach(this.items, function (item) { | |||||
| maxHeight = Math.max(maxHeight, item.top + item.height); | |||||
| }); | |||||
| } | |||||
| else { | |||||
| // orientation == 'bottom' | |||||
| util.forEach(this.items, function (item) { | |||||
| maxHeight = Math.max(maxHeight, frameHeight - item.top); | |||||
| }); | |||||
| } | |||||
| changed += update(this, 'height', maxHeight + options.margin.axis); | |||||
| } | |||||
| // 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); | |||||
| }; | |||||
| /** | |||||
| * Set data | |||||
| * @param {DataSet | Array | DataTable} data | |||||
| */ | |||||
| ItemSet.prototype.setData = function(data) { | |||||
| // unsubscribe from current dataset | |||||
| var current = this.data; | |||||
| if (current) { | |||||
| util.forEach(this.listeners, function (callback, event) { | |||||
| current.unsubscribe(event, callback); | |||||
| }); | |||||
| } | |||||
| if (data instanceof DataSet) { | |||||
| this.data = data; | |||||
| } | |||||
| else { | |||||
| this.data = new DataSet({ | |||||
| fieldTypes: { | |||||
| start: 'Date', | |||||
| end: 'Date' | |||||
| } | |||||
| }); | |||||
| this.data.add(data); | |||||
| } | |||||
| var id = this.id; | |||||
| var me = this; | |||||
| util.forEach(this.listeners, function (callback, event) { | |||||
| me.data.subscribe(event, callback, id); | |||||
| }); | |||||
| var dataItems = this.data.get({filter: ['id']}); | |||||
| var ids = []; | |||||
| util.forEach(dataItems, function (dataItem, index) { | |||||
| ids[index] = dataItem.id; | |||||
| }); | |||||
| this._onAdd(ids); | |||||
| }; | |||||
| /** | |||||
| * Handle updated items | |||||
| * @param {Number[]} ids | |||||
| * @private | |||||
| */ | |||||
| ItemSet.prototype._onUpdate = function(ids) { | |||||
| this._toQueue(ids, 'update'); | |||||
| }; | |||||
| /** | |||||
| * Handle changed items | |||||
| * @param {Number[]} ids | |||||
| * @private | |||||
| */ | |||||
| ItemSet.prototype._onAdd = function(ids) { | |||||
| this._toQueue(ids, 'add'); | |||||
| }; | |||||
| /** | |||||
| * Handle removed items | |||||
| * @param {Number[]} ids | |||||
| * @private | |||||
| */ | |||||
| ItemSet.prototype._onRemove = function(ids) { | |||||
| this._toQueue(ids, 'remove'); | |||||
| }; | |||||
| /** | |||||
| * Put items in the queue to be added/updated/remove | |||||
| * @param {Number[]} ids | |||||
| * @param {String} action can be 'add', 'update', 'remove' | |||||
| */ | |||||
| ItemSet.prototype._toQueue = function (ids, action) { | |||||
| var items = this.items; | |||||
| var queue = this.queue; | |||||
| ids.forEach(function (id) { | |||||
| var entry = queue[id]; | |||||
| if (entry) { | |||||
| // already queued, update the action of the entry | |||||
| entry.action = action; | |||||
| } | |||||
| else { | |||||
| // not yet queued, add an entry to the queue | |||||
| queue[id] = { | |||||
| item: items[id] || null, | |||||
| action: action | |||||
| }; | |||||
| } | |||||
| }); | |||||
| if (this.controller) { | |||||
| //this.requestReflow(); | |||||
| this.requestRepaint(); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Calculate the factor 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() { | |||||
| 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); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Convert a position on screen (pixels) to a datetime | |||||
| * Before this method can be used, the method _updateConversion must be | |||||
| * executed once. | |||||
| * @param {int} x Position on the screen in pixels | |||||
| * @return {Date} time The datetime the corresponds with given position x | |||||
| */ | |||||
| ItemSet.prototype.toTime = function(x) { | |||||
| var conversion = this.conversion; | |||||
| return new Date(x / conversion.factor + conversion.offset); | |||||
| }; | |||||
| /** | |||||
| * Convert a datetime (Date object) into a position on the screen | |||||
| * Before this method can be used, the method _updateConversion must be | |||||
| * executed once. | |||||
| * @param {Date} time A date | |||||
| * @return {int} x The position on the screen in pixels which corresponds | |||||
| * with the given date. | |||||
| */ | |||||
| ItemSet.prototype.toScreen = function(time) { | |||||
| var conversion = this.conversion; | |||||
| return (time.valueOf() - conversion.offset) * conversion.factor; | |||||
| }; | |||||
| @ -0,0 +1,101 @@ | |||||
| /** | |||||
| * A panel can contain components | |||||
| * @param {Component} [parent] | |||||
| * @param {Component[]} [depends] Components on which this components depends | |||||
| * (except for the parent) | |||||
| * @param {Object} [options] Available parameters: | |||||
| * {String | Number | function} [left] | |||||
| * {String | Number | function} [top] | |||||
| * {String | Number | function} [width] | |||||
| * {String | Number | function} [height] | |||||
| * {String | function} [className] | |||||
| * @constructor Panel | |||||
| * @extends Component | |||||
| */ | |||||
| function Panel(parent, depends, options) { | |||||
| this.id = util.randomUUID(); | |||||
| this.parent = parent; | |||||
| this.depends = depends; | |||||
| this.options = {}; | |||||
| this.setOptions(options); | |||||
| } | |||||
| Panel.prototype = new Component(); | |||||
| /** | |||||
| * Get the container element of the panel, which can be used by a child to | |||||
| * add its own widgets. | |||||
| * @returns {HTMLElement} container | |||||
| */ | |||||
| Panel.prototype.getContainer = function () { | |||||
| return this.frame; | |||||
| }; | |||||
| /** | |||||
| * Repaint the component | |||||
| * @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'; | |||||
| if (options.className) { | |||||
| if (typeof options.className == 'function') { | |||||
| util.addClassName(frame, String(options.className())); | |||||
| } | |||||
| else { | |||||
| util.addClassName(frame, String(options.className)); | |||||
| } | |||||
| } | |||||
| this.frame = frame; | |||||
| changed += 1; | |||||
| } | |||||
| 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; | |||||
| } | |||||
| 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); | |||||
| }; | |||||
| /** | |||||
| * Reflow the component | |||||
| * @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; | |||||
| } | |||||
| return (changed > 0); | |||||
| }; | |||||
| @ -0,0 +1,200 @@ | |||||
| /** | |||||
| * A root panel can hold components. The root panel must be initialized with | |||||
| * a DOM element as container. | |||||
| * @param {HTMLElement} container | |||||
| * @param {Object} [options] Available parameters: see RootPanel.setOptions. | |||||
| * @constructor RootPanel | |||||
| * @extends Panel | |||||
| */ | |||||
| function RootPanel(container, options) { | |||||
| this.id = util.randomUUID(); | |||||
| this.container = container; | |||||
| this.options = { | |||||
| autoResize: true | |||||
| }; | |||||
| this.listeners = {}; // event listeners | |||||
| this.setOptions(options); | |||||
| } | |||||
| RootPanel.prototype = new Panel(); | |||||
| /** | |||||
| * Set options. Will extend the current options. | |||||
| * @param {Object} [options] Available parameters: | |||||
| * {String | function} [className] | |||||
| * {String | Number | function} [left] | |||||
| * {String | Number | function} [top] | |||||
| * {String | Number | function} [width] | |||||
| * {String | Number | function} [height] | |||||
| * {String | Number | function} [height] | |||||
| * {Boolean | function} [autoResize] | |||||
| */ | |||||
| RootPanel.prototype.setOptions = function (options) { | |||||
| util.extend(this.options, options); | |||||
| if (this.options.autoResize) { | |||||
| this._watch(); | |||||
| } | |||||
| else { | |||||
| this._unwatch(); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Repaint the component | |||||
| * @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'); | |||||
| frame.className = 'graph panel'; | |||||
| if (options.className) { | |||||
| util.addClassName(frame, util.option.asString(options.className)); | |||||
| } | |||||
| 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 += 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(); | |||||
| return (changed > 0); | |||||
| }; | |||||
| /** | |||||
| * Reflow the component | |||||
| * @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); | |||||
| }; | |||||
| /** | |||||
| * Watch for changes in the size of the frame. On resize, the Panel will | |||||
| * automatically redraw itself. | |||||
| * @private | |||||
| */ | |||||
| RootPanel.prototype._watch = function () { | |||||
| var me = this; | |||||
| this._unwatch(); | |||||
| var checkSize = function () { | |||||
| if (!me.options.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(); | |||||
| } | |||||
| } | |||||
| }; | |||||
| // TODO: automatically cleanup the event listener when the frame is deleted | |||||
| util.addEventListener(window, 'resize', checkSize); | |||||
| this.watchTimer = setInterval(checkSize, 1000); | |||||
| }; | |||||
| /** | |||||
| * Stop watching for a resize of the frame. | |||||
| * @private | |||||
| */ | |||||
| RootPanel.prototype._unwatch = function () { | |||||
| if (this.watchTimer) { | |||||
| clearInterval(this.watchTimer); | |||||
| this.watchTimer = undefined; | |||||
| } | |||||
| // TODO: remove event listener on window.resize | |||||
| }; | |||||
| /** | |||||
| * Event handler | |||||
| * @param {String} event name of the event, for example 'click', 'mousemove' | |||||
| * @param {function} callback callback handler, invoked with the raw HTML Event | |||||
| * 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(); | |||||
| }; | |||||
| /** | |||||
| * Update the event listeners for all event emitters | |||||
| * @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); | |||||
| } | |||||
| } | |||||
| }); | |||||
| // TODO: be able to delete event listeners | |||||
| // TODO: be able to move event listeners to a parent when available | |||||
| } | |||||
| }; | |||||
| @ -0,0 +1,521 @@ | |||||
| /** | |||||
| * A horizontal time axis | |||||
| * @param {Component} parent | |||||
| * @param {Component[]} [depends] Components on which this components depends | |||||
| * (except for the parent) | |||||
| * @param {Object} [options] See TimeAxis.setOptions for the available | |||||
| * options. | |||||
| * @constructor TimeAxis | |||||
| * @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 | |||||
| } | |||||
| }; | |||||
| this.options = { | |||||
| orientation: 'bottom', // supported: 'top', 'bottom' | |||||
| // TODO: implement timeaxis orientations 'left' and 'right' | |||||
| showMinorLabels: true, | |||||
| showMajorLabels: true | |||||
| }; | |||||
| this.conversion = null; | |||||
| this.range = null; | |||||
| this.setOptions(options); | |||||
| } | |||||
| TimeAxis.prototype = new Component(); | |||||
| // TODO: comment options | |||||
| TimeAxis.prototype.setOptions = function (options) { | |||||
| util.extend(this.options, options); | |||||
| }; | |||||
| /** | |||||
| * Set a range (start and end) | |||||
| * @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; | |||||
| }; | |||||
| /** | |||||
| * Convert a position on screen (pixels) to a datetime | |||||
| * @param {int} x Position on the screen in pixels | |||||
| * @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); | |||||
| }; | |||||
| /** | |||||
| * Convert a datetime (Date object) into a position on the screen | |||||
| * @param {Date} time A date | |||||
| * @return {int} x The position on the screen in pixels which corresponds | |||||
| * with the given date. | |||||
| * @private | |||||
| */ | |||||
| TimeAxis.prototype.toScreen = function(time) { | |||||
| var conversion = this.conversion; | |||||
| return (time.valueOf() - conversion.offset) * conversion.factor; | |||||
| }; | |||||
| /** | |||||
| * Repaint the component | |||||
| * @return {Boolean} changed | |||||
| */ | |||||
| TimeAxis.prototype.repaint = function () { | |||||
| var changed = 0, | |||||
| update = util.updateProperty, | |||||
| asSize = util.option.asSize, | |||||
| options = this.options, | |||||
| props = this.props, | |||||
| step = this.step; | |||||
| var frame = this.frame; | |||||
| if (!frame) { | |||||
| frame = document.createElement('div'); | |||||
| this.frame = frame; | |||||
| changed += 1; | |||||
| } | |||||
| frame.className = 'axis ' + options.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 orientation = options.orientation; | |||||
| var defaultTop = (orientation == 'bottom') ? (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 (options.showMinorLabels) { | |||||
| this._repaintMinorText(x, step.getLabelMinor()); | |||||
| } | |||||
| if (isMajor && options.showMajorLabels) { | |||||
| if (x > 0) { | |||||
| if (xFirstMajorLabel == undefined) { | |||||
| xFirstMajorLabel = x; | |||||
| } | |||||
| this._repaintMajorText(x, step.getLabelMajor()); | |||||
| } | |||||
| this._repaintMajorLine(x); | |||||
| } | |||||
| else { | |||||
| this._repaintMinorLine(x); | |||||
| } | |||||
| step.next(); | |||||
| } | |||||
| // create a major label on the left when needed | |||||
| if (options.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); | |||||
| }; | |||||
| /** | |||||
| * Start a repaint. Move all DOM elements to a redundant list, where they | |||||
| * can be picked for re-use, or can be cleaned up in the end | |||||
| * @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 = []; | |||||
| }; | |||||
| /** | |||||
| * End a repaint. Cleanup leftover DOM elements in the redundant list | |||||
| * @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); | |||||
| } | |||||
| } | |||||
| }); | |||||
| }; | |||||
| /** | |||||
| * Create a minor label for the axis at position x | |||||
| * @param {Number} x | |||||
| * @param {String} text | |||||
| * @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 | |||||
| }; | |||||
| /** | |||||
| * Create a Major label for the axis at position x | |||||
| * @param {Number} x | |||||
| * @param {String} 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 | |||||
| }; | |||||
| /** | |||||
| * Create a minor line for the axis at position x | |||||
| * @param {Number} x | |||||
| * @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'; | |||||
| }; | |||||
| /** | |||||
| * Create a Major line for the axis at position x | |||||
| * @param {Number} 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'; | |||||
| }; | |||||
| /** | |||||
| * Repaint the horizontal line for the axis | |||||
| * @private | |||||
| */ | |||||
| TimeAxis.prototype._repaintLine = function() { | |||||
| var line = this.dom.line, | |||||
| frame = this.frame, | |||||
| options = this.options; | |||||
| // line before all axis elements | |||||
| if (options.showMinorLabels || options.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'; | |||||
| } | |||||
| else { | |||||
| if (line && axis.parentElement) { | |||||
| frame.removeChild(axis.line); | |||||
| delete this.dom.line; | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Create characters used to determine the size of text on the axis | |||||
| * @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.characterMinor) { | |||||
| 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.characterMajor) { | |||||
| text = document.createTextNode('0'); | |||||
| var measureCharMajor = document.createElement('DIV'); | |||||
| measureCharMajor.className = 'text major measure'; | |||||
| measureCharMajor.appendChild(text); | |||||
| this.frame.appendChild(measureCharMajor); | |||||
| dom.measureCharMajor = measureCharMajor; | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Reflow the component | |||||
| * @return {Boolean} resized | |||||
| */ | |||||
| TimeAxis.prototype.reflow = function () { | |||||
| 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.options.showMinorLabels, | |||||
| showMajorLabels = this.options.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.options.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.minorLineTop = -this.top; | |||||
| props.minorLineHeight = this.top + props.majorLabelHeight; | |||||
| props.minorLineWidth = 1; // TODO: really calculate width | |||||
| props.majorLineTop = -this.top; | |||||
| props.majorLineHeight = this.top + props.minorLabelHeight + props.majorLabelHeight; | |||||
| props.majorLineWidth = 1; // TODO: really calculate width | |||||
| props.lineTop = 0; | |||||
| break; | |||||
| case 'top': | |||||
| props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; | |||||
| props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0; | |||||
| props.majorLabelTop = 0; | |||||
| props.minorLabelTop = props.majorLabelTop + props.majorLabelHeight; | |||||
| props.minorLineTop = props.minorLabelTop; | |||||
| props.minorLineHeight = parentHeight - props.majorLabelHeight - this.top; | |||||
| props.minorLineWidth = 1; // TODO: really calculate width | |||||
| props.majorLineTop = 0; | |||||
| props.majorLineHeight = parentHeight - this.top; | |||||
| props.majorLineWidth = 1; // TODO: really calculate width | |||||
| props.lineTop = props.majorLabelHeight + props.minorLabelHeight; | |||||
| break; | |||||
| default: | |||||
| throw new Error('Unkown orientation "' + this.options.orientation + '"'); | |||||
| } | |||||
| 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.cast(range.start, 'Date'), | |||||
| end = util.cast(range.end, 'Date'), | |||||
| minimumStep = this.toTime((props.minorCharWidth || 10) * 5) - this.toTime(0); | |||||
| this.step = new TimeStep(start, end, minimumStep); | |||||
| changed += update(props.range, 'start', start.valueOf()); | |||||
| changed += update(props.range, 'end', end.valueOf()); | |||||
| changed += update(props.range, 'minimumStep', minimumStep.valueOf()); | |||||
| } | |||||
| return (changed > 0); | |||||
| }; | |||||
| /** | |||||
| * Calculate the factor 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'); | |||||
| } | |||||
| if (range.conversion) { | |||||
| this.conversion = range.conversion(this.width); | |||||
| } | |||||
| else { | |||||
| this.conversion = Range.conversion(range.start, range.end, this.width); | |||||
| } | |||||
| }; | |||||
| @ -0,0 +1,139 @@ | |||||
| /** | |||||
| * @constructor Controller | |||||
| * | |||||
| * A Controller controls the reflows and repaints of all visual components | |||||
| */ | |||||
| function Controller () { | |||||
| this.id = util.randomUUID(); | |||||
| this.components = {}; | |||||
| this.repaintTimer = undefined; | |||||
| this.reflowTimer = undefined; | |||||
| } | |||||
| /** | |||||
| * Add a component to the controller | |||||
| * @param {Component | Controller} component | |||||
| */ | |||||
| Controller.prototype.add = function (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; | |||||
| }; | |||||
| /** | |||||
| * Request a reflow. The controller will schedule a reflow | |||||
| */ | |||||
| Controller.prototype.requestReflow = function () { | |||||
| if (!this.reflowTimer) { | |||||
| var me = this; | |||||
| this.reflowTimer = setTimeout(function () { | |||||
| me.reflowTimer = undefined; | |||||
| me.reflow(); | |||||
| }, 0); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Request a repaint. The controller will schedule a repaint | |||||
| */ | |||||
| Controller.prototype.requestRepaint = function () { | |||||
| if (!this.repaintTimer) { | |||||
| var me = this; | |||||
| this.repaintTimer = setTimeout(function () { | |||||
| me.repaintTimer = undefined; | |||||
| me.repaint(); | |||||
| }, 0); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Repaint all components | |||||
| */ | |||||
| Controller.prototype.repaint = function () { | |||||
| 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; | |||||
| } | |||||
| } | |||||
| util.forEach(this.components, repaint); | |||||
| // 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 () { | |||||
| 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; | |||||
| } | |||||
| } | |||||
| util.forEach(this.components, reflow); | |||||
| // immediately repaint when needed | |||||
| if (resized) { | |||||
| this.repaint(); | |||||
| } | |||||
| // TODO: limit the number of nested reflows/repaints, prevent loop | |||||
| }; | |||||
| @ -0,0 +1,502 @@ | |||||
| /** | |||||
| * DataSet | |||||
| * | |||||
| * Usage: | |||||
| * var dataSet = new DataSet({ | |||||
| * fieldId: '_id', | |||||
| * fieldTypes: { | |||||
| * // ... | |||||
| * } | |||||
| * }); | |||||
| * | |||||
| * dataSet.add(item); | |||||
| * dataSet.add(data); | |||||
| * dataSet.update(item); | |||||
| * dataSet.update(data); | |||||
| * dataSet.remove(id); | |||||
| * dataSet.remove(ids); | |||||
| * var data = dataSet.get(); | |||||
| * var data = dataSet.get(id); | |||||
| * var data = dataSet.get(ids); | |||||
| * var data = dataSet.get(ids, options, data); | |||||
| * dataSet.clear(); | |||||
| * | |||||
| * A data set can: | |||||
| * - add/remove/update data | |||||
| * - gives triggers upon changes in the data | |||||
| * - can import/export data in various data formats | |||||
| * @param {Object} [options] Available options: | |||||
| * {String} fieldId Field name of the id in the | |||||
| * items, 'id' by default. | |||||
| * {Object.<String, String} fieldTypes | |||||
| * A map with field names as key, | |||||
| * and the field type as value. | |||||
| */ | |||||
| function DataSet (options) { | |||||
| var me = this; | |||||
| this.options = options || {}; | |||||
| this.data = {}; // map with data indexed by id | |||||
| this.fieldId = this.options.fieldId || 'id'; // name of the field containing id | |||||
| this.fieldTypes = {}; // field types by field name | |||||
| if (this.options.fieldTypes) { | |||||
| util.forEach(this.options.fieldTypes, function (value, field) { | |||||
| if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') { | |||||
| me.fieldTypes[field] = 'Date'; | |||||
| } | |||||
| else { | |||||
| me.fieldTypes[field] = value; | |||||
| } | |||||
| }); | |||||
| } | |||||
| // event subscribers | |||||
| this.subscribers = {}; | |||||
| this.internalIds = {}; // internally generated id's | |||||
| } | |||||
| /** | |||||
| * Subscribe to an event, add an event listener | |||||
| * @param {String} event Event name. Available events: 'put', 'update', | |||||
| * 'remove' | |||||
| * @param {function} callback Callback method. Called with three parameters: | |||||
| * {String} event | |||||
| * {Object | null} params | |||||
| * {String} senderId | |||||
| * @param {String} [id] Optional id for the sender, used to filter | |||||
| * events triggered by the sender itself. | |||||
| */ | |||||
| DataSet.prototype.subscribe = function (event, callback, id) { | |||||
| var subscribers = this.subscribers[event]; | |||||
| if (!subscribers) { | |||||
| subscribers = []; | |||||
| this.subscribers[event] = subscribers; | |||||
| } | |||||
| subscribers.push({ | |||||
| id: id ? String(id) : null, | |||||
| callback: callback | |||||
| }); | |||||
| }; | |||||
| /** | |||||
| * Unsubscribe from an event, remove an event listener | |||||
| * @param {String} event | |||||
| * @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); | |||||
| }); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Trigger an event | |||||
| * @param {String} event | |||||
| * @param {Object | null} params | |||||
| * @param {String} [senderId] Optional id of the sender. The event will | |||||
| * be triggered for all subscribers except the | |||||
| * sender itself. | |||||
| * @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['*']); | |||||
| } | |||||
| subscribers.forEach(function (listener) { | |||||
| if (listener.id != senderId && listener.callback) { | |||||
| listener.callback(event, params, senderId || null); | |||||
| } | |||||
| }); | |||||
| }; | |||||
| /** | |||||
| * Add data. Existing items with the same id will be overwritten. | |||||
| * @param {Object | Array | DataTable} data | |||||
| * @param {String} [senderId] Optional sender id, used to trigger events for | |||||
| * all but this sender's event subscribers. | |||||
| */ | |||||
| DataSet.prototype.add = function (data, senderId) { | |||||
| var items = [], | |||||
| id, | |||||
| me = this; | |||||
| if (data instanceof Array) { | |||||
| // Array | |||||
| data.forEach(function (item) { | |||||
| var id = me._addItem(item); | |||||
| items.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 = {}; | |||||
| columns.forEach(function (field, col) { | |||||
| item[field] = data.getValue(row, col); | |||||
| }); | |||||
| id = me._addItem(item); | |||||
| items.push(id); | |||||
| } | |||||
| } | |||||
| else if (data instanceof Object) { | |||||
| // Single item | |||||
| id = me._addItem(data); | |||||
| items.push(id); | |||||
| } | |||||
| else { | |||||
| throw new Error('Unknown dataType'); | |||||
| } | |||||
| this._trigger('add', {items: items}, senderId); | |||||
| }; | |||||
| /** | |||||
| * Update existing items. Items with the same id will be merged | |||||
| * @param {Object | Array | DataTable} data | |||||
| * @param {String} [senderId] Optional sender id, used to trigger events for | |||||
| * all but this sender's event subscribers. | |||||
| */ | |||||
| DataSet.prototype.update = function (data, senderId) { | |||||
| var items = [], | |||||
| id, | |||||
| me = this; | |||||
| if (data instanceof Array) { | |||||
| // Array | |||||
| data.forEach(function (item) { | |||||
| var id = me._updateItem(item); | |||||
| items.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 = {}; | |||||
| columns.forEach(function (field, col) { | |||||
| item[field] = data.getValue(row, col); | |||||
| }); | |||||
| id = me._updateItem(item); | |||||
| items.push(id); | |||||
| } | |||||
| } | |||||
| else if (data instanceof Object) { | |||||
| // Single item | |||||
| id = me._updateItem(data); | |||||
| items.push(id); | |||||
| } | |||||
| else { | |||||
| throw new Error('Unknown dataType'); | |||||
| } | |||||
| this._trigger('update', {items: items}, senderId); | |||||
| }; | |||||
| /** | |||||
| * Get a data item or multiple items | |||||
| * @param {String | Number | Array | Object} [ids] Id of a single item, or an | |||||
| * array with multiple id's, or | |||||
| * undefined or an Object with options | |||||
| * to retrieve all data. | |||||
| * @param {Object} [options] Available options: | |||||
| * {String} [type] | |||||
| * 'DataTable' or 'Array' (default) | |||||
| * {Object.<String, String>} [fieldTypes] | |||||
| * {String[]} [fields] filter fields | |||||
| * @param {Array | DataTable} [data] If provided, items will be appended | |||||
| * to this array or table. Required | |||||
| * in case of Google DataTable | |||||
| * @return {Array | Object | DataTable | null} data | |||||
| * @throws Error | |||||
| */ | |||||
| DataSet.prototype.get = function (ids, options, data) { | |||||
| var me = this; | |||||
| // shift arguments when first argument contains the options | |||||
| if (util.getType(ids) == 'Object') { | |||||
| data = options; | |||||
| options = ids; | |||||
| ids = undefined; | |||||
| } | |||||
| // merge field types | |||||
| var fieldTypes = {}; | |||||
| if (this.options && this.options.fieldTypes) { | |||||
| util.forEach(this.options.fieldTypes, function (value, field) { | |||||
| fieldTypes[field] = value; | |||||
| }); | |||||
| } | |||||
| if (options && options.fieldTypes) { | |||||
| util.forEach(options.fieldTypes, function (value, field) { | |||||
| fieldTypes[field] = value; | |||||
| }); | |||||
| } | |||||
| var fields = options ? options.fields : undefined; | |||||
| // 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'; | |||||
| } | |||||
| if (type == 'DataTable') { | |||||
| // return a Google DataTable | |||||
| var columns = this._getColumnNames(data); | |||||
| if (ids == undefined) { | |||||
| // return all data | |||||
| util.forEach(this.data, function (item) { | |||||
| me._appendRow(data, columns, me._castItem(item)); | |||||
| }); | |||||
| } | |||||
| else if (util.isNumber(ids) || util.isString(ids)) { | |||||
| var item = me._castItem(me.data[ids], fieldTypes, fields); | |||||
| this._appendRow(data, columns, item); | |||||
| } | |||||
| else if (ids instanceof Array) { | |||||
| ids.forEach(function (id) { | |||||
| var item = me._castItem(me.data[id], fieldTypes, fields); | |||||
| me._appendRow(data, columns, item); | |||||
| }); | |||||
| } | |||||
| else { | |||||
| throw new TypeError('Parameter "ids" must be ' + | |||||
| 'undefined, a String, Number, or Array'); | |||||
| } | |||||
| } | |||||
| else { | |||||
| // return an array | |||||
| data = data || []; | |||||
| if (ids == undefined) { | |||||
| // return all data | |||||
| util.forEach(this.data, function (item) { | |||||
| data.push(me._castItem(item, fieldTypes, fields)); | |||||
| }); | |||||
| } | |||||
| else if (util.isNumber(ids) || util.isString(ids)) { | |||||
| // return a single item | |||||
| return this._castItem(me.data[ids], fieldTypes, fields); | |||||
| } | |||||
| else if (ids instanceof Array) { | |||||
| ids.forEach(function (id) { | |||||
| data.push(me._castItem(me.data[id], fieldTypes, fields)); | |||||
| }); | |||||
| } | |||||
| else { | |||||
| throw new TypeError('Parameter "ids" must be ' + | |||||
| 'undefined, a String, Number, or Array'); | |||||
| } | |||||
| } | |||||
| return data; | |||||
| }; | |||||
| /** | |||||
| * Remove an object by pointer or by id | |||||
| * @param {String | Number | Object | Array} id Object or id, or an array with | |||||
| * objects or ids to be removed | |||||
| * @param {String} [senderId] Optional sender id, used to trigger events for | |||||
| * all but this sender's event subscribers. | |||||
| */ | |||||
| DataSet.prototype.remove = function (id, senderId) { | |||||
| var items = [], | |||||
| me = this; | |||||
| if (util.isNumber(id) || util.isString(id)) { | |||||
| delete this.data[id]; | |||||
| delete this.internalIds[id]; | |||||
| items.push(id); | |||||
| } | |||||
| else if (id instanceof Array) { | |||||
| id.forEach(function (id) { | |||||
| me.remove(id); | |||||
| }); | |||||
| items = items.concat(id); | |||||
| } | |||||
| else if (id instanceof Object) { | |||||
| // search for the object | |||||
| for (var i in this.data) { | |||||
| if (this.data.hasOwnProperty(i)) { | |||||
| if (this.data[i] == id) { | |||||
| delete this.data[i]; | |||||
| delete this.internalIds[i]; | |||||
| items.push(i); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| this._trigger('remove', {items: items}, senderId); | |||||
| }; | |||||
| /** | |||||
| * Clear the data | |||||
| * @param {String} [senderId] Optional sender id, used to trigger events for | |||||
| * all but this sender's event subscribers. | |||||
| */ | |||||
| DataSet.prototype.clear = function (senderId) { | |||||
| var items = Object.keys(this.data); | |||||
| this.data = []; | |||||
| this.internalIds = {}; | |||||
| this._trigger('remove', {items: items}, senderId); | |||||
| }; | |||||
| /** | |||||
| * Add a single item | |||||
| * @param {Object} item | |||||
| * @return {String} id | |||||
| * @private | |||||
| */ | |||||
| DataSet.prototype._addItem = function (item) { | |||||
| var id = item[this.fieldId]; | |||||
| if (id == undefined) { | |||||
| // 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 type = this.fieldTypes[field]; // type may be undefined | |||||
| d[field] = util.cast(item[field], type); | |||||
| } | |||||
| } | |||||
| this.data[id] = d; | |||||
| //TODO: fail when an item with this id already exists? | |||||
| return id; | |||||
| }; | |||||
| /** | |||||
| * Cast and filter the fields of an item | |||||
| * @param {Object | undefined} item | |||||
| * @param {Object.<String, String>} [fieldTypes] | |||||
| * @param {String[]} [fields] | |||||
| * @return {Object | null} castedItem | |||||
| * @private | |||||
| */ | |||||
| DataSet.prototype._castItem = function (item, fieldTypes, fields) { | |||||
| var clone, | |||||
| fieldId = this.fieldId, | |||||
| internalIds = this.internalIds; | |||||
| if (item) { | |||||
| clone = {}; | |||||
| fieldTypes = fieldTypes || {}; | |||||
| if (fields) { | |||||
| // output filtered fields | |||||
| util.forEach(item, function (value, field) { | |||||
| if (fields.indexOf(field) != -1) { | |||||
| clone[field] = util.cast(value, fieldTypes[field]); | |||||
| } | |||||
| }); | |||||
| } | |||||
| else { | |||||
| // output all fields, except internal ids | |||||
| util.forEach(item, function (value, field) { | |||||
| if (field != fieldId || !(value in internalIds)) { | |||||
| clone[field] = util.cast(value, fieldTypes[field]); | |||||
| } | |||||
| }); | |||||
| } | |||||
| } | |||||
| else { | |||||
| clone = null; | |||||
| } | |||||
| return clone; | |||||
| }; | |||||
| /** | |||||
| * Update a single item: merge with existing item | |||||
| * @param {Object} item | |||||
| * @return {String} id | |||||
| * @private | |||||
| */ | |||||
| DataSet.prototype._updateItem = function (item) { | |||||
| var id = item[this.fieldId]; | |||||
| if (id == undefined) { | |||||
| throw new Error('Item has no id (item: ' + JSON.stringify(item) + ')'); | |||||
| } | |||||
| var d = this.data[id]; | |||||
| if (d) { | |||||
| // merge with current item | |||||
| for (var field in item) { | |||||
| if (item.hasOwnProperty(field)) { | |||||
| var type = this.fieldTypes[field]; // type may be undefined | |||||
| d[field] = util.cast(item[field], type); | |||||
| } | |||||
| } | |||||
| } | |||||
| else { | |||||
| // create new item | |||||
| this._addItem(item); | |||||
| } | |||||
| return id; | |||||
| }; | |||||
| /** | |||||
| * Get an array with the column names of a Google DataTable | |||||
| * @param {DataTable} dataTable | |||||
| * @return {Array} columnNames | |||||
| * @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; | |||||
| }; | |||||
| /** | |||||
| * Append an item as a row to the dataTable | |||||
| * @param dataTable | |||||
| * @param columns | |||||
| * @param item | |||||
| * @private | |||||
| */ | |||||
| DataSet.prototype._appendRow = function (dataTable, columns, item) { | |||||
| var row = dataTable.addRow(); | |||||
| columns.forEach(function (field, col) { | |||||
| dataTable.setValue(row, col, item[field]); | |||||
| }); | |||||
| }; | |||||
| @ -0,0 +1,116 @@ | |||||
| /** | |||||
| * Event listener (singleton) | |||||
| */ | |||||
| 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); | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * 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]; | |||||
| } | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * 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); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| @ -0,0 +1,62 @@ | |||||
| <!DOCTYPE HTML> | |||||
| <html> | |||||
| <head> | |||||
| <title>Timeline demo</title> | |||||
| <script src="../timeline.js"></script> | |||||
| <link href="../timeline.css" rel="stylesheet" type="text/css" /> | |||||
| <style> | |||||
| body, html { | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| padding: 0; | |||||
| margin: 0; | |||||
| font-family: arial, sans-serif; | |||||
| font-size: 12pt; | |||||
| } | |||||
| #visualization { | |||||
| box-sizing: border-box; | |||||
| padding: 10px; | |||||
| width: 100%; | |||||
| height: 300px; | |||||
| } | |||||
| #visualization .itemset { | |||||
| /*background: rgba(255, 255, 0, 0.5);*/ | |||||
| } | |||||
| </style> | |||||
| </head> | |||||
| <body> | |||||
| <div id="visualization"></div> | |||||
| <script> | |||||
| // create a dataset with items | |||||
| var now = moment().minutes(0).seconds(0).milliseconds(0); | |||||
| var data = new DataSet({ | |||||
| fieldTypes: { | |||||
| start: 'Date', | |||||
| end: 'Date' | |||||
| } | |||||
| }); | |||||
| data.add([ | |||||
| {id: 1, content: 'item 1<br>start', start: now.clone().add('days', 4).toDate()}, | |||||
| {id: 2, content: 'item 2', start: now.clone().add('days', -2).toDate() }, | |||||
| {id: 3, content: 'item 3', start: now.clone().add('days', 2).toDate()}, | |||||
| {id: 4, content: 'item 4', start: now.clone().add('days', 0).toDate(), end: now.clone().add('days', 3).toDate()}, | |||||
| {id: 5, content: 'item 5', start: now.clone().add('days', 9).toDate(), type:'point'}, | |||||
| {id: 6, content: 'item 6', start: now.clone().add('days', 11).toDate()} | |||||
| ]); | |||||
| var container = document.getElementById('visualization'); | |||||
| var options = { | |||||
| start: now.clone().add('days', -3).valueOf(), | |||||
| end: now.clone().add('days', 7).valueOf() | |||||
| }; | |||||
| var timeline = new Timeline(container, data, options); | |||||
| </script> | |||||
| </body> | |||||
| </html> | |||||
| @ -0,0 +1,24 @@ | |||||
| /** | |||||
| * @@name | |||||
| * https://github.com/almende/vis | |||||
| * | |||||
| * A dynamic, browser-based visualization library. | |||||
| * | |||||
| * @version @@version | |||||
| * @date @@date | |||||
| * | |||||
| * @license | |||||
| * Copyright (C) 2011-2013 Almende B.V, http://almende.com | |||||
| * | |||||
| * 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 | |||||
| * | |||||
| * 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. | |||||
| */ | |||||
| @ -0,0 +1,524 @@ | |||||
| /** | |||||
| * @constructor Range | |||||
| * A Range controls a numeric range with a start and end value. | |||||
| * The Range adjusts the range based on mouse events or programmatic changes, | |||||
| * and triggers events when the range is changing or has been changed. | |||||
| * @param {Object} [options] See description at Range.setOptions | |||||
| * @extends Controller | |||||
| */ | |||||
| function Range(options) { | |||||
| this.id = util.randomUUID(); | |||||
| this.start = 0; // Number | |||||
| this.end = 0; // Number | |||||
| this.options = { | |||||
| min: null, | |||||
| max: null, | |||||
| zoomMin: null, | |||||
| zoomMax: null | |||||
| }; | |||||
| this.setOptions(options); | |||||
| this.listeners = []; | |||||
| } | |||||
| /** | |||||
| * Set options for the range controller | |||||
| * @param {Object} options Available options: | |||||
| * {Number} start Set start value of the range | |||||
| * {Number} end Set end value of the range | |||||
| * {Number} min Minimum value for start | |||||
| * {Number} max Maximum value for end | |||||
| * {Number} zoomMin Set a minimum value for | |||||
| * (end - start). | |||||
| * {Number} zoomMax Set a maximum value for | |||||
| * (end - start). | |||||
| */ | |||||
| Range.prototype.setOptions = function (options) { | |||||
| util.extend(this.options, options); | |||||
| if (options.start != null || options.end != null) { | |||||
| this.setRange(options.start, options.end); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Add listeners for mouse and touch events to the component | |||||
| * @param {Component} component | |||||
| * @param {String} event Available events: 'move', 'zoom' | |||||
| * @param {String} direction Available directions: 'horizontal', 'vertical' | |||||
| */ | |||||
| Range.prototype.subscribe = function (component, event, direction) { | |||||
| var me = this; | |||||
| var listener; | |||||
| if (direction != 'horizontal' && direction != 'vertical') { | |||||
| throw new TypeError('Unknown direction "' + direction + '". ' + | |||||
| 'Choose "horizontal" or "vertical".'); | |||||
| } | |||||
| //noinspection FallthroughInSwitchStatementJS | |||||
| if (event == 'move') { | |||||
| listener = { | |||||
| component: component, | |||||
| event: event, | |||||
| direction: direction, | |||||
| callback: function (event) { | |||||
| me._onMouseDown(event, listener); | |||||
| }, | |||||
| params: {} | |||||
| }; | |||||
| component.on('mousedown', listener.callback); | |||||
| me.listeners.push(listener); | |||||
| } | |||||
| else if (event == 'zoom') { | |||||
| listener = { | |||||
| component: component, | |||||
| event: event, | |||||
| direction: direction, | |||||
| callback: function (event) { | |||||
| me._onMouseWheel(event, listener); | |||||
| }, | |||||
| params: {} | |||||
| }; | |||||
| component.on('mousewheel', listener.callback); | |||||
| me.listeners.push(listener); | |||||
| } | |||||
| else { | |||||
| throw new TypeError('Unknown event "' + event + '". ' + | |||||
| 'Choose "move" or "zoom".'); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Event handler | |||||
| * @param {String} event name of the event, for example 'click', 'mousemove' | |||||
| * @param {function} callback callback handler, invoked with the raw HTML Event | |||||
| * as parameter. | |||||
| */ | |||||
| Range.prototype.on = function (event, callback) { | |||||
| events.addListener(this, event, callback); | |||||
| }; | |||||
| /** | |||||
| * Trigger an event | |||||
| * @param {String} event name of the event, available events: 'rangechange', | |||||
| * 'rangechanged' | |||||
| * @private | |||||
| */ | |||||
| Range.prototype._trigger = function (event) { | |||||
| events.trigger(this, event, { | |||||
| start: this.start, | |||||
| end: this.end | |||||
| }); | |||||
| }; | |||||
| /** | |||||
| * Set a new start and end range | |||||
| * @param {Number} start | |||||
| * @param {Number} end | |||||
| */ | |||||
| Range.prototype.setRange = function(start, end) { | |||||
| var changed = this._applyRange(start, end); | |||||
| if (changed) { | |||||
| this._trigger('rangechange'); | |||||
| this._trigger('rangechanged'); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Set a new start and end range. This method is the same as setRange, but | |||||
| * does not trigger a range change and range changed event, and it returns | |||||
| * true when the range is changed | |||||
| * @param {Number} start | |||||
| * @param {Number} end | |||||
| * @return {Boolean} changed | |||||
| * @private | |||||
| */ | |||||
| Range.prototype._applyRange = function(start, end) { | |||||
| var newStart = util.cast(start, 'Number'); | |||||
| var newEnd = util.cast(end, 'Number'); | |||||
| var diff; | |||||
| // check for valid number | |||||
| if (newStart == null || isNaN(newStart)) { | |||||
| throw new Error('Invalid start "' + start + '"'); | |||||
| } | |||||
| if (newEnd == null || isNaN(newEnd)) { | |||||
| throw new Error('Invalid end "' + end + '"'); | |||||
| } | |||||
| // prevent start < end | |||||
| if (newEnd < newStart) { | |||||
| newEnd = newStart; | |||||
| } | |||||
| // prevent start < min | |||||
| if (this.options.min != null) { | |||||
| var min = this.options.min.valueOf(); | |||||
| if (newStart < min) { | |||||
| diff = (min - newStart); | |||||
| newStart += diff; | |||||
| newEnd += diff; | |||||
| } | |||||
| } | |||||
| // prevent end > max | |||||
| if (this.options.max != null) { | |||||
| var max = this.options.max.valueOf(); | |||||
| if (newEnd > max) { | |||||
| diff = (newEnd - max); | |||||
| newStart -= diff; | |||||
| newEnd -= diff; | |||||
| } | |||||
| } | |||||
| // prevent (end-start) > zoomMin | |||||
| if (this.options.zoomMin != null) { | |||||
| var zoomMin = this.options.zoomMin.valueOf(); | |||||
| if (zoomMin < 0) { | |||||
| zoomMin = 0; | |||||
| } | |||||
| if ((newEnd - newStart) < zoomMin) { | |||||
| if ((this.end - this.start) > zoomMin) { | |||||
| // zoom to the minimum | |||||
| diff = (zoomMin - (newEnd - newStart)); | |||||
| newStart -= diff / 2; | |||||
| newEnd += diff / 2; | |||||
| } | |||||
| else { | |||||
| // ingore this action, we are already zoomed to the minimum | |||||
| newStart = this.start; | |||||
| newEnd = this.end; | |||||
| } | |||||
| } | |||||
| } | |||||
| // prevent (end-start) > zoomMin | |||||
| if (this.options.zoomMax != null) { | |||||
| var zoomMax = this.options.zoomMax.valueOf(); | |||||
| if (zoomMax < 0) { | |||||
| zoomMax = 0; | |||||
| } | |||||
| if ((newEnd - newStart) > zoomMax) { | |||||
| if ((this.end - this.start) < zoomMax) { | |||||
| // zoom to the maximum | |||||
| diff = ((newEnd - newStart) - zoomMax); | |||||
| newStart += diff / 2; | |||||
| newEnd -= diff / 2; | |||||
| } | |||||
| else { | |||||
| // ingore this action, we are already zoomed to the maximum | |||||
| newStart = this.start; | |||||
| newEnd = this.end; | |||||
| } | |||||
| } | |||||
| } | |||||
| var changed = (this.start != newStart || this.end != newEnd); | |||||
| this.start = newStart; | |||||
| this.end = newEnd; | |||||
| return changed; | |||||
| }; | |||||
| /** | |||||
| * Retrieve the current range. | |||||
| * @return {Object} An object with start and end properties | |||||
| */ | |||||
| Range.prototype.getRange = function() { | |||||
| return { | |||||
| start: this.start, | |||||
| end: this.end | |||||
| }; | |||||
| }; | |||||
| /** | |||||
| * Calculate the conversion offset and factor for current range, based on | |||||
| * the provided width | |||||
| * @param {Number} width | |||||
| * @returns {{offset: number, factor: number}} conversion | |||||
| */ | |||||
| Range.prototype.conversion = function (width) { | |||||
| var start = this.start; | |||||
| var end = this.end; | |||||
| return Range.conversion(this.start, this.end, width); | |||||
| }; | |||||
| /** | |||||
| * Static method to calculate the conversion offset and factor 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 | |||||
| */ | |||||
| 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 | |||||
| }; | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Start moving horizontally or vertically | |||||
| * @param {Event} event | |||||
| * @param {Object} listener Listener containing the component and params | |||||
| * @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'; | |||||
| } | |||||
| // 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); | |||||
| } | |||||
| util.preventDefault(event); | |||||
| }; | |||||
| /** | |||||
| * Perform moving operating. | |||||
| * This function activated from within the funcion TimeAxis._onMouseDown(). | |||||
| * @param {Event} event | |||||
| * @param {Object} listener | |||||
| * @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; | |||||
| } | |||||
| 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; | |||||
| } | |||||
| 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); | |||||
| // fire a rangechange event | |||||
| this._trigger('rangechange'); | |||||
| util.preventDefault(event); | |||||
| }; | |||||
| /** | |||||
| * Stop moving operating. | |||||
| * This function activated from within the function Range._onMouseDown(). | |||||
| * @param {event} event | |||||
| * @param {Object} listener | |||||
| * @private | |||||
| */ | |||||
| Range.prototype._onMouseUp = function (event, listener) { | |||||
| event = event || window.event; | |||||
| 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 (params.moved) { | |||||
| // 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 | |||||
| * @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; | |||||
| } | |||||
| } | |||||
| me.zoom(zoomFactor, zoomAround); | |||||
| }; | |||||
| zoom(); | |||||
| } | |||||
| // 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); | |||||
| }; | |||||
| /** | |||||
| * 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 | |||||
| */ | |||||
| 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; | |||||
| } | |||||
| // 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; | |||||
| } | |||||
| // 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); | |||||
| } | |||||
| // zoom start and end relative to the zoomAround value | |||||
| var startDiff = (this.start - zoomAround); | |||||
| var endDiff = (this.end - zoomAround); | |||||
| // calculate new start and end | |||||
| var newStart = this.start - startDiff * zoomFactor; | |||||
| var newEnd = this.end - endDiff * zoomFactor; | |||||
| 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 | |||||
| */ | |||||
| Range.prototype.move = function(moveFactor) { | |||||
| // zoom start Date and end Date relative to the zoomAroundDate | |||||
| var diff = (this.end - this.start); | |||||
| // apply new values | |||||
| var newStart = this.start + diff * moveFactor; | |||||
| var newEnd = this.end + diff * moveFactor; | |||||
| // TODO: reckon with min and max range | |||||
| this.start = newStart; | |||||
| this.end = newEnd; | |||||
| }; | |||||
| @ -0,0 +1,157 @@ | |||||
| /** | |||||
| * @constructor Stack | |||||
| * Stacks items on top of each other. | |||||
| * @param {ItemSet} parent | |||||
| * @param {Object} [options] | |||||
| */ | |||||
| function Stack (parent, options) { | |||||
| this.parent = parent; | |||||
| this.options = { | |||||
| order: function (a, b) { | |||||
| return (b.width - a.width) || (a.left - b.left); | |||||
| } | |||||
| }; | |||||
| this.ordered = []; // ordered items | |||||
| this.setOptions(options); | |||||
| } | |||||
| /** | |||||
| * Set options for the stack | |||||
| * @param {Object} options Available options: | |||||
| * {ItemSet} parent | |||||
| * {Number} margin | |||||
| * {function} order Stacking order | |||||
| */ | |||||
| Stack.prototype.setOptions = function (options) { | |||||
| util.extend(this.options, options); | |||||
| // TODO: register on data changes at the connected parent itemset, and update the changed part only and immediately | |||||
| }; | |||||
| /** | |||||
| * Stack the items such that they don't overlap. The items will have a minimal | |||||
| * distance equal to options.margin.item. | |||||
| */ | |||||
| Stack.prototype.update = function() { | |||||
| this._order(); | |||||
| this._stack(); | |||||
| }; | |||||
| /** | |||||
| * Order the items. The items are ordered by width first, and by left position | |||||
| * second. | |||||
| * If a custom order function has been provided via the options, then this will | |||||
| * be used. | |||||
| * @private | |||||
| */ | |||||
| Stack.prototype._order = function() { | |||||
| 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; | |||||
| util.forEach(items, function (item, id) { | |||||
| ordered[index] = item; | |||||
| index++; | |||||
| }); | |||||
| //if a customer stack order function exists, use it. | |||||
| var order = this.options.order; | |||||
| if (!(typeof this.options.order === 'function')) { | |||||
| throw new Error('Option order must be a function'); | |||||
| } | |||||
| ordered.sort(order); | |||||
| this.ordered = ordered; | |||||
| }; | |||||
| /** | |||||
| * Adjust vertical positions of the events such that they don't overlap each | |||||
| * other. | |||||
| * @private | |||||
| */ | |||||
| Stack.prototype._stack = function() { | |||||
| var i, | |||||
| iMax, | |||||
| ordered = this.ordered, | |||||
| options = this.options, | |||||
| axisOnTop = (options.orientation == 'top'), | |||||
| margin = options.margin && options.margin.item || 0; | |||||
| // 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); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Check if the destiny position of given item overlaps with any | |||||
| * of the other items from index itemStart to itemEnd. | |||||
| * @param {Array} items Array with items | |||||
| * @param {int} itemIndex Number of the item to be checked for overlap | |||||
| * @param {int} itemStart First item to be checked. | |||||
| * @param {int} itemEnd Last item to be checked. | |||||
| * @return {Object | null} colliding item, or undefined when no collisions | |||||
| * @param {Number} margin A minimum required margin. | |||||
| * If margin is provided, the two items will be | |||||
| * marked colliding when they overlap or | |||||
| * when the margin between the two is smaller than | |||||
| * the requested margin. | |||||
| */ | |||||
| Stack.prototype.checkOverlap = function(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; | |||||
| } | |||||
| } | |||||
| } | |||||
| return null; | |||||
| }; | |||||
| /** | |||||
| * Test if the two provided items collide | |||||
| * The items must have parameters left, width, top, and height. | |||||
| * @param {Component} a The first item | |||||
| * @param {Component} b The second item | |||||
| * @param {Number} margin A minimum required margin. | |||||
| * If margin is provided, the two items will be | |||||
| * marked colliding when they overlap or | |||||
| * when the margin between the two is smaller than | |||||
| * the requested margin. | |||||
| * @return {boolean} true if a and b collide, else false | |||||
| */ | |||||
| Stack.prototype.collision = function(a, b, margin) { | |||||
| return ((a.left - margin) < (b.left + b.width) && | |||||
| (a.left + a.width + margin) > b.left && | |||||
| (a.top - margin) < (b.top + b.height) && | |||||
| (a.top + a.height + margin) > b.top); | |||||
| }; | |||||
| @ -0,0 +1,450 @@ | |||||
| /** | |||||
| * @constructor TimeStep | |||||
| * The class TimeStep is an iterator for dates. You provide a start date and an | |||||
| * end date. The class itself determines the best scale (step size) based on the | |||||
| * provided start Date, end Date, and minimumStep. | |||||
| * | |||||
| * If minimumStep is provided, the step size is chosen as close as possible | |||||
| * to the minimumStep but larger than minimumStep. If minimumStep is not | |||||
| * provided, the scale is set to 1 DAY. | |||||
| * The minimumStep should correspond with the onscreen size of about 6 characters | |||||
| * | |||||
| * Alternatively, you can set a scale by hand. | |||||
| * After creation, you can initialize the class by executing first(). Then you | |||||
| * can iterate from the start date to the end date via next(). You can check if | |||||
| * the end date is reached with the function hasNext(). After each step, you can | |||||
| * retrieve the current date via getCurrent(). | |||||
| * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours, | |||||
| * days, to years. | |||||
| * | |||||
| * Version: 1.2 | |||||
| * | |||||
| * @param {Date} [start] The start date, for example new Date(2010, 9, 21) | |||||
| * or new Date(2010, 9, 21, 23, 45, 00) | |||||
| * @param {Date} [end] The end date | |||||
| * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds | |||||
| */ | |||||
| TimeStep = function(start, end, minimumStep) { | |||||
| // variables | |||||
| this.current = new Date(); | |||||
| this._start = new Date(); | |||||
| this._end = new Date(); | |||||
| this.autoScale = true; | |||||
| this.scale = TimeStep.SCALE.DAY; | |||||
| this.step = 1; | |||||
| // 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 | |||||
| }; | |||||
| /** | |||||
| * Set a new range | |||||
| * If minimumStep is provided, the step size is chosen as close as possible | |||||
| * to the minimumStep but larger than minimumStep. If minimumStep is not | |||||
| * provided, the scale is set to 1 DAY. | |||||
| * The minimumStep should correspond with the onscreen size of about 6 characters | |||||
| * @param {Date} start The start date and time. | |||||
| * @param {Date} end The end date and time. | |||||
| * @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"; | |||||
| return; | |||||
| } | |||||
| 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); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Set the range iterator to the start date. | |||||
| */ | |||||
| TimeStep.prototype.first = function() { | |||||
| this.current = new Date(this._start.valueOf()); | |||||
| this.roundToMinor(); | |||||
| }; | |||||
| /** | |||||
| * Round the current date to the first minor date value | |||||
| * 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 | |||||
| } | |||||
| 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; | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Check if the there is a next step | |||||
| * @return {boolean} true if the current date has not passed the end date | |||||
| */ | |||||
| TimeStep.prototype.hasNext = function () { | |||||
| 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; | |||||
| } | |||||
| } | |||||
| 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; | |||||
| } | |||||
| } | |||||
| // safety mechanism: if current time is still unchanged, move to the end | |||||
| if (this.current.valueOf() == prev) { | |||||
| this.current = new Date(this._end.valueOf()); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Get the current datetime | |||||
| * @return {Date} current The current date | |||||
| */ | |||||
| TimeStep.prototype.getCurrent = function() { | |||||
| return this.current; | |||||
| }; | |||||
| /** | |||||
| * Set a custom scale. Autoscaling will be disabled. | |||||
| * For example setScale(SCALE.MINUTES, 5) will result | |||||
| * in minor steps of 5 minutes, and major steps of an hour. | |||||
| * | |||||
| * @param {TimeStep.SCALE} newScale | |||||
| * A scale. Choose from SCALE.MILLISECOND, | |||||
| * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR, | |||||
| * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH, | |||||
| * SCALE.YEAR. | |||||
| * @param {Number} newStep A step size, by default 1. Choose for | |||||
| * example 1, 2, 5, or 10. | |||||
| */ | |||||
| TimeStep.prototype.setScale = function(newScale, newStep) { | |||||
| this.scale = newScale; | |||||
| if (newStep > 0) { | |||||
| this.step = newStep; | |||||
| } | |||||
| this.autoScale = false; | |||||
| }; | |||||
| /** | |||||
| * Enable or disable autoscaling | |||||
| * @param {boolean} enable If true, autoascaling is set true | |||||
| */ | |||||
| TimeStep.prototype.setAutoScale = function (enable) { | |||||
| this.autoScale = enable; | |||||
| }; | |||||
| /** | |||||
| * Automatically determine the scale that bests fits the provided minimum step | |||||
| * @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;} | |||||
| }; | |||||
| /** | |||||
| * Snap a date to a rounded value. The snap intervals are dependent on the | |||||
| * current scale and step. | |||||
| * @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); | |||||
| } | |||||
| 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 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); | |||||
| 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.MILLISECOND) { | |||||
| var step = this.step > 5 ? this.step / 2 : 1; | |||||
| date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Check if the current value is a major value (for example when the step | |||||
| * is DAY, a major value is each first day of the MONTH) | |||||
| * @return {boolean} true if current date is major, else false. | |||||
| */ | |||||
| 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; | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Returns formatted text for the minor axislabel, depending on the current | |||||
| * date and the scale. For example when scale is MINUTE, the current time is | |||||
| * formatted as "hh:mm". | |||||
| * @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 ''; | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Returns formatted text for the major axislabel, depending on the current | |||||
| * date and the scale. For example when scale is MINUTE, the major scale is | |||||
| * hours, and the hour will be formatted as "hh". | |||||
| * @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 ''; | |||||
| } | |||||
| }; | |||||
| @ -0,0 +1,732 @@ | |||||
| // create namespace | |||||
| var util = {}; | |||||
| /** | |||||
| * Test whether given object is a number | |||||
| * @param {*} object | |||||
| * @return {Boolean} isNumber | |||||
| */ | |||||
| util.isNumber = function isNumber(object) { | |||||
| return (object instanceof Number || typeof object == 'number'); | |||||
| }; | |||||
| /** | |||||
| * Test whether given object is a string | |||||
| * @param {*} object | |||||
| * @return {Boolean} isString | |||||
| */ | |||||
| util.isString = function isString(object) { | |||||
| return (object instanceof String || typeof object == 'string'); | |||||
| }; | |||||
| /** | |||||
| * Test whether given object is a Date, or a String containing a Date | |||||
| * @param {Date | String} object | |||||
| * @return {Boolean} isDate | |||||
| */ | |||||
| util.isDate = function isDate(object) { | |||||
| 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 (!isNaN(Date.parse(object))) { | |||||
| return true; | |||||
| } | |||||
| } | |||||
| return false; | |||||
| }; | |||||
| /** | |||||
| * Test whether given object is an instance of google.visualization.DataTable | |||||
| * @param {*} object | |||||
| * @return {Boolean} isDataTable | |||||
| */ | |||||
| util.isDataTable = function isDataTable(object) { | |||||
| return (typeof (google) !== 'undefined') && | |||||
| (google.visualization) && | |||||
| (google.visualization.DataTable) && | |||||
| (object instanceof google.visualization.DataTable); | |||||
| }; | |||||
| /** | |||||
| * Create a semi UUID | |||||
| * source: http://stackoverflow.com/a/105074/1262753 | |||||
| * @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() | |||||
| ); | |||||
| }; | |||||
| /** | |||||
| * Extend object a with the properties of object b | |||||
| * @param {Object} a | |||||
| * @param {Object} b | |||||
| * @return {Object} a | |||||
| */ | |||||
| util.extend = function (a, b) { | |||||
| for (var prop in b) { | |||||
| if (b.hasOwnProperty(prop)) { | |||||
| a[prop] = b[prop]; | |||||
| } | |||||
| } | |||||
| return a; | |||||
| }; | |||||
| /** | |||||
| * Cast an object to another type | |||||
| * @param {Boolean | Number | String | Date | Null | undefined} object | |||||
| * @param {String | function | undefined} type Name of the type or a cast | |||||
| * function. Available types: | |||||
| * 'Boolean', 'Number', 'String', | |||||
| * 'Date', ISODate', 'ASPDate'. | |||||
| * @return {*} object | |||||
| * @throws Error | |||||
| */ | |||||
| util.cast = function cast(object, type) { | |||||
| if (object === undefined) { | |||||
| return undefined; | |||||
| } | |||||
| if (object === null) { | |||||
| return null; | |||||
| } | |||||
| if (!type) { | |||||
| return object; | |||||
| } | |||||
| if (typeof type == 'function') { | |||||
| return type(object); | |||||
| } | |||||
| //noinspection FallthroughInSwitchStatementJS | |||||
| switch (type) { | |||||
| case 'boolean': | |||||
| case 'Boolean': | |||||
| return Boolean(object); | |||||
| case 'number': | |||||
| case 'Number': | |||||
| return Number(object); | |||||
| 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()); | |||||
| } | |||||
| if (util.isString(object)) { | |||||
| // parse ASP.Net Date pattern, | |||||
| // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/' | |||||
| // code from http://momentjs.com/ | |||||
| var match = ASPDateRegex.exec(object); | |||||
| if (match) { | |||||
| return new Date(Number(match[1])); | |||||
| } | |||||
| else { | |||||
| return new Date(object); | |||||
| } | |||||
| } | |||||
| else { | |||||
| throw new Error( | |||||
| 'Cannot cast object of type ' + util.getType(object) + | |||||
| ' to type Date'); | |||||
| } | |||||
| case 'ISODate': | |||||
| if (object instanceof Date) { | |||||
| return object.toISOString(); | |||||
| } | |||||
| else if (util.isNumber(object) || util.isString(Object)) { | |||||
| return (new Date(object)).toISOString() | |||||
| } | |||||
| else { | |||||
| throw new Error( | |||||
| 'Cannot cast object of type ' + util.getType(object) + | |||||
| ' to type ISODate'); | |||||
| } | |||||
| case 'ASPDate': | |||||
| if (object instanceof Date) { | |||||
| return '/Date(' + object.valueOf() + ')/'; | |||||
| } | |||||
| else if (util.isNumber(object) || util.isString(Object)) { | |||||
| return '/Date(' + (new Date(object)).valueOf() + ')/'; | |||||
| } | |||||
| else { | |||||
| throw new Error( | |||||
| 'Cannot cast object of type ' + util.getType(object) + | |||||
| ' to type ASPDate'); | |||||
| } | |||||
| default: | |||||
| throw new Error('Cannot cast object of type ' + util.getType(object) + | |||||
| ' to type "' + type + '"'); | |||||
| } | |||||
| }; | |||||
| var ASPDateRegex = /^\/?Date\((\-?\d+)/i; | |||||
| /** | |||||
| * Get the type of an object | |||||
| * @param {*} object | |||||
| * @return {String} type | |||||
| */ | |||||
| util.getType = function getType(object) { | |||||
| var type = typeof object; | |||||
| if (type == 'object') { | |||||
| if (object == null) { | |||||
| return 'null'; | |||||
| } | |||||
| if (object && object.constructor && object.constructor.name) { | |||||
| return object.constructor.name; | |||||
| } | |||||
| } | |||||
| return type; | |||||
| }; | |||||
| /** | |||||
| * Retrieve the absolute left value of a DOM element | |||||
| * @param {Element} elem A dom element, for example a div | |||||
| * @return {number} left The absolute left position of this element | |||||
| * 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; | |||||
| }; | |||||
| /** | |||||
| * Retrieve the absolute top value of a DOM element | |||||
| * @param {Element} elem A dom element, for example a div | |||||
| * @return {number} top The absolute top position of this element | |||||
| * 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; | |||||
| }; | |||||
| /** | |||||
| * Get the absolute, vertical mouse position from an event. | |||||
| * @param {Event} event | |||||
| * @return {Number} pageY | |||||
| */ | |||||
| util.getPageY = function getPageY (event) { | |||||
| if ('pageY' in event) { | |||||
| return event.pageY; | |||||
| } | |||||
| 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 ); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Get the absolute, horizontal mouse position from an event. | |||||
| * @param {Event} event | |||||
| * @return {Number} pageX | |||||
| */ | |||||
| util.getPageX = function getPageX (event) { | |||||
| if ('pageY' in event) { | |||||
| return event.pageX; | |||||
| } | |||||
| 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 ); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * add a className to the given elements style | |||||
| * @param {Element} elem | |||||
| * @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(' '); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * add a className to the given elements style | |||||
| * @param {Element} elem | |||||
| * @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(' '); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * For each method for both arrays and objects. | |||||
| * In case of an array, the built-in Array.forEach() is applied. | |||||
| * In case of an Object, the method loops over all properties of the object. | |||||
| * @param {Object | Array} object An Object or Array | |||||
| * @param {function} callback Callback method, called for each item in | |||||
| * the object or array with three parameters: | |||||
| * callback(value, index, object) | |||||
| */ | |||||
| util.forEach = function forEach (object, callback) { | |||||
| if (object instanceof Array) { | |||||
| // array | |||||
| object.forEach(callback); | |||||
| } | |||||
| else { | |||||
| // object | |||||
| for (var key in object) { | |||||
| if (object.hasOwnProperty(key)) { | |||||
| callback(object[key], key, object); | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Update a property in an object | |||||
| * @param {Object} object | |||||
| * @param {String} key | |||||
| * @param {*} value | |||||
| * @return {Boolean} changed | |||||
| */ | |||||
| util.updateProperty = function updateProp (object, key, value) { | |||||
| if (object[key] !== value) { | |||||
| object[key] = value; | |||||
| return true; | |||||
| } | |||||
| else { | |||||
| return false; | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Add and event listener. Works for all browsers | |||||
| * @param {Element} element An html element | |||||
| * @param {string} action The action, for example "click", | |||||
| * without the prefix "on" | |||||
| * @param {function} listener The callback function to be executed | |||||
| * @param {boolean} [useCapture] | |||||
| */ | |||||
| util.addEventListener = function addEventListener(element, action, listener, useCapture) { | |||||
| 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 | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Remove an event listener from an element | |||||
| * @param {Element} element An html dom element | |||||
| * @param {string} action The name of the event, for example "mousedown" | |||||
| * @param {function} listener The listener function | |||||
| * @param {boolean} [useCapture] | |||||
| */ | |||||
| util.removeEventListener = function removeEventListener(element, action, listener, useCapture) { | |||||
| 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); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Get HTML element which is the target of the event | |||||
| * @param {Event} event | |||||
| * @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; | |||||
| }; | |||||
| /** | |||||
| * 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 | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * 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 | |||||
| } | |||||
| }; | |||||
| util.option = {}; | |||||
| /** | |||||
| * Cast a value as boolean | |||||
| * @param {Boolean | function | undefined} value | |||||
| * @param {Boolean} [defaultValue] | |||||
| * @returns {Boolean} bool | |||||
| */ | |||||
| util.option.asBoolean = function (value, defaultValue) { | |||||
| if (typeof value == 'function') { | |||||
| value = value(); | |||||
| } | |||||
| if (value != null) { | |||||
| return (value != false); | |||||
| } | |||||
| return defaultValue || null; | |||||
| }; | |||||
| /** | |||||
| * Cast a value as string | |||||
| * @param {String | function | undefined} value | |||||
| * @param {String} [defaultValue] | |||||
| * @returns {String} str | |||||
| */ | |||||
| util.option.asString = function (value, defaultValue) { | |||||
| if (typeof value == 'function') { | |||||
| value = value(); | |||||
| } | |||||
| if (value != null) { | |||||
| return String(value); | |||||
| } | |||||
| return defaultValue || null; | |||||
| }; | |||||
| /** | |||||
| * Cast a size or location in pixels or a percentage | |||||
| * @param {String | Number | function | undefined} value | |||||
| * @param {String} [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; | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Cast a value as DOM element | |||||
| * @param {HTMLElement | function | undefined} value | |||||
| * @param {HTMLElement} [defaultValue] | |||||
| * @returns {HTMLElement | null} dom | |||||
| */ | |||||
| util.option.asElement = function (value, defaultValue) { | |||||
| if (typeof value == 'function') { | |||||
| value = value(); | |||||
| } | |||||
| return value || defaultValue || null; | |||||
| }; | |||||
| // Internet Explorer 8 and older does not support Array.indexOf, so we define | |||||
| // 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) { | |||||
| } | |||||
| } | |||||
| // 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); | |||||
| } | |||||
| } | |||||
| } | |||||
| // Internet Explorer 8 and older does not support Array.map, so we define it | |||||
| // here in that case. | |||||
| // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/map | |||||
| // 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; | |||||
| 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); | |||||
| // 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"); | |||||
| } | |||||
| // 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); | |||||
| // 7. Let k be 0 | |||||
| k = 0; | |||||
| // 8. Repeat, while k < len | |||||
| while(k < len) { | |||||
| 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) { | |||||
| // 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); | |||||
| // 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 }); | |||||
| // 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"; | |||||
| if (this == null) { | |||||
| 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); | |||||
| } | |||||
| } | |||||
| return res; | |||||
| }; | |||||
| } | |||||
| // Internet Explorer 8 and older does not support Object.keys, so we define it | |||||
| // 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; | |||||
| } | |||||
| })() | |||||
| } | |||||
| @ -0,0 +1,118 @@ | |||||
| /** | |||||
| * Create a timeline visualization | |||||
| * @param {HTMLElement} container | |||||
| * @param {DataSet | Array | DataTable} [data] | |||||
| * @param {Object} [options] See Timeline.setOptions for the available options. | |||||
| * @constructor | |||||
| */ | |||||
| function Timeline (container, data, options) { | |||||
| var me = this; | |||||
| this.options = { | |||||
| orientation: 'bottom', | |||||
| zoomMin: 10, // milliseconds | |||||
| zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds | |||||
| moveable: true, | |||||
| zoomable: true | |||||
| }; | |||||
| // controller | |||||
| this.controller = new Controller(); | |||||
| // main panel | |||||
| if (!container) { | |||||
| throw new Error('No container element provided'); | |||||
| } | |||||
| this.main = new RootPanel(container, { | |||||
| autoResize: false, | |||||
| height: function () { | |||||
| return me.timeaxis.height + me.itemset.height; | |||||
| } | |||||
| }); | |||||
| this.controller.add(this.main); | |||||
| // range | |||||
| var now = moment().minutes(0).seconds(0).milliseconds(0); | |||||
| var start = options.start && options.start.valueOf() || now.clone().add('days', -3).valueOf(); | |||||
| var end = options.end && options.end.valueOf() || moment(start).clone().add('days', 7).valueOf(); | |||||
| // TODO: if start and end are not provided, calculate range from the dataset | |||||
| this.range = new Range({ | |||||
| start: start, | |||||
| end: end | |||||
| }); | |||||
| // TODO: reckon with options moveable and zoomable | |||||
| this.range.subscribe(this.main, 'move', 'horizontal'); | |||||
| this.range.subscribe(this.main, 'zoom', 'horizontal'); | |||||
| this.range.on('rangechange', function () { | |||||
| // TODO: fix the delay in reflow/repaint, does not feel snappy | |||||
| me.controller.requestReflow(); | |||||
| }); | |||||
| this.range.on('rangechanged', function () { | |||||
| me.controller.requestReflow(); | |||||
| }); | |||||
| // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable | |||||
| // time axis | |||||
| this.timeaxis = new TimeAxis(this.main, null, { | |||||
| orientation: this.options.orientation, | |||||
| range: this.range | |||||
| }); | |||||
| this.timeaxis.setRange(this.range); | |||||
| this.controller.add(this.timeaxis); | |||||
| // items panel | |||||
| this.itemset = new ItemSet(this.main, [this.timeaxis], { | |||||
| orientation: this.options.orientation, | |||||
| range: this.range, | |||||
| data: data | |||||
| }); | |||||
| this.itemset.setRange(this.range); | |||||
| if (data) { | |||||
| this.setData(data); | |||||
| } | |||||
| this.controller.add(this.itemset); | |||||
| this.setOptions(options); | |||||
| } | |||||
| /** | |||||
| * Set options | |||||
| * @param {Object} options TODO: describe the available options | |||||
| */ | |||||
| Timeline.prototype.setOptions = function (options) { | |||||
| util.extend(this.options, options); | |||||
| // update options the timeaxis | |||||
| this.timeaxis.setOptions(this.options); | |||||
| // update options for the range | |||||
| this.range.setOptions(this.options); | |||||
| // update options the itemset | |||||
| var top, | |||||
| me = this; | |||||
| if (this.options.orientation == 'top') { | |||||
| top = function () { | |||||
| return me.timeaxis.height; | |||||
| } | |||||
| } | |||||
| else { | |||||
| top = function () { | |||||
| return me.main.height - me.timeaxis.height - me.itemset.height; | |||||
| } | |||||
| } | |||||
| this.itemset.setOptions({ | |||||
| orientation: this.options.orientation, | |||||
| top: top | |||||
| }); | |||||
| this.controller.repaint(); | |||||
| }; | |||||
| /** | |||||
| * Set data | |||||
| * @param {DataSet | Array | DataTable} data | |||||
| */ | |||||
| Timeline.prototype.setData = function(data) { | |||||
| this.itemset.setData(data); | |||||
| }; | |||||
| @ -0,0 +1,78 @@ | |||||
| <!DOCTYPE HTML> | |||||
| <html> | |||||
| <head> | |||||
| <title></title> | |||||
| <script src="../lib/moment.min.js"></script> | |||||
| <script src="../src/util.js"></script> | |||||
| <script src="../src/events.js"></script> | |||||
| <script src="../src/range.js"></script> | |||||
| <script src="../src/timestep.js"></script> | |||||
| <script src="../src/dataset.js"></script> | |||||
| <script type="text/javascript" src="http://www.google.com/jsapi"></script> | |||||
| </head> | |||||
| <body> | |||||
| <script> | |||||
| var dataset = new DataSet({ | |||||
| fieldId: 'id', | |||||
| fieldTypes: { | |||||
| start: 'ISODate' | |||||
| } | |||||
| }); | |||||
| // create an anonymous event listener | |||||
| dataset.subscribe('*', function (event, params, id) { | |||||
| console.log('anonymous listener ', event, params, id); | |||||
| }); | |||||
| // create a named event listener | |||||
| var entityId = '123'; | |||||
| dataset.subscribe('*', function (event, params, id) { | |||||
| console.log('named listener ', event, params, id); | |||||
| }, entityId); | |||||
| // anonymous put | |||||
| dataset.add([ | |||||
| {id: 1, content: 'item 1', start: new Date()}, | |||||
| {id: 2, content: 'item 2', start: (new Date()).valueOf()}, | |||||
| {id: '3', content: 'item 3', start: (new Date()).toISOString()}, | |||||
| {id: 4, content: 'item 4', start: '/Date(1198908717056)/'}, | |||||
| {id: 5, content: 'item 5', start: undefined}, | |||||
| {content: 'item 6', start: new Date()} | |||||
| ]); | |||||
| // named update | |||||
| dataset.update([ | |||||
| {id: 1, foo: 'bar'} | |||||
| ], entityId); | |||||
| google.load("visualization", "1"); | |||||
| google.setOnLoadCallback(function () { | |||||
| var table = new google.visualization.DataTable(); | |||||
| table.addColumn('datetime', 'start'); | |||||
| table.addColumn('datetime', 'end'); | |||||
| table.addColumn('string', 'content'); | |||||
| table.addRows([ | |||||
| [new Date(2010,7,23), , 'Conversation<br>' + | |||||
| '<img src="img/comments-icon.png" style="width:32px; height:32px;">'], | |||||
| [new Date(2010,7,23,23,0,0), , 'Mail from boss<br>' + | |||||
| '<img src="img/mail-icon.png" style="width:32px; height:32px;">'], | |||||
| [new Date(2010,7,24,16,0,0), , 'Report'], | |||||
| [new Date(2010,7,26), new Date(2010,8,2), 'Traject A'], | |||||
| [new Date(2010,7,28), , 'Memo<br>' + | |||||
| '<img src="img/notes-edit-icon.png" style="width:48px; height:48px;">'], | |||||
| [new Date(2010,7,29), , 'Phone call<br>' + | |||||
| '<img src="img/Hardware-Mobile-Phone-icon.png" style="width:32px; height:32px;">'], | |||||
| [new Date(2010,7,31), new Date(2010,8,3), 'Traject B'], | |||||
| [new Date(2010,8,4,12,0,0), , 'Report<br>' + | |||||
| '<img src="img/attachment-icon.png" style="width:32px; height:32px;">'] | |||||
| ]); | |||||
| dataset.add(table); | |||||
| }); | |||||
| </script> | |||||
| </body> | |||||
| </html> | |||||
| @ -0,0 +1,87 @@ | |||||
| <!DOCTYPE HTML> | |||||
| <html> | |||||
| <head> | |||||
| <title></title> | |||||
| <script src="../lib/moment.min.js"></script> | |||||
| <script src="../src/util.js"></script> | |||||
| <script src="../src/events.js"></script> | |||||
| <script src="../src/timestep.js"></script> | |||||
| <script src="../src/dataset.js"></script> | |||||
| <script src="../src/stack.js"></script> | |||||
| <script src="../src/controller.js"></script> | |||||
| <script src="../src/range.js"></script> | |||||
| <script src="../src/component/component.js"></script> | |||||
| <script src="../src/component/panel.js"></script> | |||||
| <script src="../src/component/rootpanel.js"></script> | |||||
| <script src="../src/component/timeaxis.js"></script> | |||||
| <script src="../src/component/itemset.js"></script> | |||||
| <script src="../src/component/item/item.js"></script> | |||||
| <script src="../src/component/item/itembox.js"></script> | |||||
| <script src="../src/component/item/itemrange.js"></script> | |||||
| <script src="../src/component/item/itempoint.js"></script> | |||||
| <script src="../src/visualization/timeline.js"></script> | |||||
| <link href="../src/component/css/panel.css" rel="stylesheet" type="text/css" /> | |||||
| <link href="../src/component/css/timeaxis.css" rel="stylesheet" type="text/css" /> | |||||
| <link href="../src/component/css/item.css" rel="stylesheet" type="text/css" /> | |||||
| <style> | |||||
| body, html { | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| padding: 0; | |||||
| margin: 0; | |||||
| font-family: arial, sans-serif; | |||||
| font-size: 12pt; | |||||
| } | |||||
| #visualization { | |||||
| box-sizing: border-box; | |||||
| padding: 10px; | |||||
| width: 100%; | |||||
| height: 300px; | |||||
| } | |||||
| #visualization .itemset { | |||||
| /*background: rgba(255, 255, 0, 0.5);*/ | |||||
| } | |||||
| </style> | |||||
| </head> | |||||
| <body> | |||||
| <div id="visualization"></div> | |||||
| <script> | |||||
| // create a dataset with items | |||||
| var now = moment().minutes(0).seconds(0).milliseconds(0); | |||||
| var data = new DataSet({ | |||||
| fieldTypes: { | |||||
| start: 'Date', | |||||
| end: 'Date' | |||||
| } | |||||
| }); | |||||
| data.add([ | |||||
| {id: 1, content: 'item 1<br>start', start: now.clone().add('days', 4).toDate()}, | |||||
| {id: 2, content: 'item 2', start: now.clone().add('days', -2).toDate() }, | |||||
| {id: 3, content: 'item 3', start: now.clone().add('days', 2).toDate()}, | |||||
| {id: 4, content: 'item 4', start: now.clone().add('days', 0).toDate(), end: now.clone().add('days', 3).toDate()}, | |||||
| {id: 5, content: 'item 5', start: now.clone().add('days', 9).toDate(), type:'point'}, | |||||
| {id: 6, content: 'item 6', start: now.clone().add('days', 11).toDate()} | |||||
| ]); | |||||
| var container = document.getElementById('visualization'); | |||||
| var options = { | |||||
| start: now.clone().add('days', -3).valueOf(), | |||||
| end: now.clone().add('days', 7).valueOf(), | |||||
| min: moment('2013-01-01').valueOf(), | |||||
| max: moment('2013-12-31').valueOf(), | |||||
| // zoomMin: 1000 * 60 * 60, // 1 hour | |||||
| zoomMin: 1000 * 60 * 60 * 24, // 1 hour | |||||
| zoomMax: 1000 * 60 * 60 * 24 * 30 * 6 // 6 months | |||||
| }; | |||||
| var timeline = new Timeline(container, data, options); | |||||
| </script> | |||||
| </body> | |||||
| </html> | |||||
| @ -0,0 +1,36 @@ | |||||
| <!DOCTYPE HTML> | |||||
| <html> | |||||
| <head> | |||||
| <title></title> | |||||
| <script src="../lib/moment.min.js"></script> | |||||
| <script src="../src/util.js"></script> | |||||
| <script src="../src/events.js"></script> | |||||
| <script src="../src/component/panel.js"></script> | |||||
| <script src="../src/timestep.js"></script> | |||||
| </head> | |||||
| <body> | |||||
| <script> | |||||
| var diffs = [ | |||||
| 1000, | |||||
| 1000 * 60, | |||||
| 1000 * 60 * 60, | |||||
| 1000 * 60 * 60 * 24, | |||||
| 1000 * 60 * 60 * 24 * 30, | |||||
| 1000 * 60 * 60 * 24 * 30 * 100, | |||||
| 1000 * 60 * 60 * 24 * 30 * 10000 | |||||
| ]; | |||||
| diffs.forEach(function (diff) { | |||||
| var step = new TimeStep(new Date(), new Date((new Date()).valueOf() + diff), diff / 40); | |||||
| console.log(diff, step._start.toLocaleString(), step._end.toLocaleString(), step.scale, step.step); | |||||
| step.first(); | |||||
| while (step.hasNext()) { | |||||
| console.log(step.getLabelMajor(), ' --- ', step.getLabelMinor()); | |||||
| step.next(); | |||||
| } | |||||
| }); | |||||
| </script> | |||||
| </body> | |||||
| </html> | |||||