Browse Source

Initial import

css_transitions
josdejong 11 years ago
parent
commit
45afd42252
37 changed files with 13502 additions and 3 deletions
  1. +2
    -0
      .gitignore
  2. +114
    -0
      Jakefile.js
  3. +176
    -0
      LICENSE
  4. +14
    -0
      NOTICE
  5. +9
    -3
      README.md
  6. +62
    -0
      bin/timeline/examples/01_basics.html
  7. +142
    -0
      bin/timeline/timeline.css
  8. +6267
    -0
      bin/timeline/timeline.js
  9. +27
    -0
      bin/timeline/timeline.min.js
  10. +1400
    -0
      lib/moment.js
  11. +6
    -0
      lib/moment.min.js
  12. +30
    -0
      package.json
  13. +116
    -0
      src/component/component.js
  14. +84
    -0
      src/component/css/item.css
  15. +9
    -0
      src/component/css/panel.css
  16. +47
    -0
      src/component/css/timeaxis.css
  17. +36
    -0
      src/component/item/item.js
  18. +270
    -0
      src/component/item/itembox.js
  19. +209
    -0
      src/component/item/itempoint.js
  20. +219
    -0
      src/component/item/itemrange.js
  21. +416
    -0
      src/component/itemset.js
  22. +101
    -0
      src/component/panel.js
  23. +200
    -0
      src/component/rootpanel.js
  24. +521
    -0
      src/component/timeaxis.js
  25. +139
    -0
      src/controller.js
  26. +502
    -0
      src/dataset.js
  27. +116
    -0
      src/events.js
  28. +62
    -0
      src/examples/timeline/01_basics.html
  29. +24
    -0
      src/header.js
  30. +524
    -0
      src/range.js
  31. +157
    -0
      src/stack.js
  32. +450
    -0
      src/timestep.js
  33. +732
    -0
      src/util.js
  34. +118
    -0
      src/visualization/timeline.js
  35. +78
    -0
      test/dataset.html
  36. +87
    -0
      test/timeline.html
  37. +36
    -0
      test/timestep.html

+ 2
- 0
.gitignore View File

@ -0,0 +1,2 @@
.idea
node_modules

+ 114
- 0
Jakefile.js View File

@ -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);
};

+ 176
- 0
LICENSE View File

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

+ 14
- 0
NOTICE View File

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

+ 9
- 3
README.md View File

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

+ 62
- 0
bin/timeline/examples/01_basics.html View File

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

+ 142
- 0
bin/timeline/timeline.css View File

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

+ 6267
- 0
bin/timeline/timeline.js
File diff suppressed because it is too large
View File


+ 27
- 0
bin/timeline/timeline.min.js
File diff suppressed because it is too large
View File


+ 1400
- 0
lib/moment.js
File diff suppressed because it is too large
View File


+ 6
- 0
lib/moment.min.js
File diff suppressed because it is too large
View File


+ 30
- 0
package.json View File

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

+ 116
- 0
src/component/component.js View File

@ -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');
}
};

+ 84
- 0
src/component/css/item.css View File

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

+ 9
- 0
src/component/css/panel.css View File

@ -0,0 +1,9 @@
.graph {
position: relative;
border: 1px solid #bfbfbf;
}
.graph .panel {
position: absolute;
}

+ 47
- 0
src/component/css/timeaxis.css View File

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

+ 36
- 0
src/component/item/item.js View File

@ -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 = {};

+ 270
- 0
src/component/item/itembox.js View File

@ -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';
}
};

+ 209
- 0
src/component/item/itempoint.js View File

@ -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';
}
};

+ 219
- 0
src/component/item/itemrange.js View File

@ -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';
}
};

+ 416
- 0
src/component/itemset.js View File

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

+ 101
- 0
src/component/panel.js View File

@ -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);
};

+ 200
- 0
src/component/rootpanel.js View File

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

+ 521
- 0
src/component/timeaxis.js View File

@ -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);
}
};

+ 139
- 0
src/controller.js View File

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

+ 502
- 0
src/dataset.js View File

@ -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]);
});
};

+ 116
- 0
src/events.js View File

@ -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);
}
}
}
}
};

+ 62
- 0
src/examples/timeline/01_basics.html View File

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

+ 24
- 0
src/header.js View File

@ -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.
*/

+ 524
- 0
src/range.js View File

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

+ 157
- 0
src/stack.js View File

@ -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);
};

+ 450
- 0
src/timestep.js View File

@ -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 '';
}
};

+ 732
- 0
src/util.js View File

@ -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;
}
})()
}

+ 118
- 0
src/visualization/timeline.js View File

@ -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);
};

+ 78
- 0
test/dataset.html View File

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

+ 87
- 0
test/timeline.html View File

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

+ 36
- 0
test/timestep.html View File

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

Loading…
Cancel
Save