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