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