Browse Source

Halfway implementation of groups

css_transitions
josdejong 11 years ago
parent
commit
8ac893855c
11 changed files with 1308 additions and 284 deletions
  1. +3
    -0
      Jakefile.js
  2. +60
    -0
      examples/timeline/05_groups.html
  3. +7
    -0
      src/component/css/groupset.css
  4. +0
    -17
      src/component/css/item.css
  5. +28
    -0
      src/component/css/itemset.css
  6. +389
    -0
      src/component/groupset.js
  7. +33
    -80
      src/component/itemset.js
  8. +1
    -1
      src/stack.js
  9. +121
    -45
      src/visualization/timeline.js
  10. +662
    -137
      vis.js
  11. +4
    -4
      vis.min.js

+ 3
- 0
Jakefile.js View File

@ -30,6 +30,8 @@ task('build', {async: true}, function () {
var result = concat({
src: [
'./src/component/css/panel.css',
'./src/component/css/groupset.css',
'./src/component/css/itemset.css',
'./src/component/css/item.css',
'./src/component/css/timeaxis.css'
],
@ -59,6 +61,7 @@ task('build', {async: true}, function () {
'./src/component/timeaxis.js',
'./src/component/itemset.js',
'./src/component/item/*.js',
'./src/component/groupset.js',
'./src/visualization/timeline.js',

+ 60
- 0
examples/timeline/05_groups.html View File

@ -0,0 +1,60 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Timeline | Group example</title>
<style>
body, html {
font-family: arial, sans-serif;
font-size: 11pt;
}
#visualization {
box-sizing: border-box;
width: 100%;
height: 300px;
}
</style>
<script src="../../vis.js"></script>
</head>
<body>
<div id="visualization"></div>
<script>
var now = moment().minutes(0).seconds(0).milliseconds(0);
// create a dataset with items
var items = new vis.DataSet();
items.add([
{id: 1, content: 'item 1<br>start', start: now.clone().add('days', 4)},
{id: 2, content: 'item 2', start: now.clone().add('days', -2)},
{id: 3, content: 'item 3', start: now.clone().add('days', 2)},
{id: 4, content: 'item 4', start: now.clone().add('days', 0), end: now.clone().add('days', 3).toDate()},
{id: 5, content: 'item 5', start: now.clone().add('days', 9), type:'point'},
{id: 6, content: 'item 6', start: now.clone().add('days', 11)}
]);
var groups = new vis.DataSet();
groups.add([
{id: 1, content: 'Group 1'},
{id: 2, content: 'Group 2'},
{id: 3, content: 'Group 3'},
{id: 4, content: 'Group 4'}
]);
var container = document.getElementById('visualization');
var options = {
start: now.clone().add('days', -3),
end: now.clone().add('days', 7),
//height: '100%'
};
var timeline = new vis.Timeline(container);
timeline.setOptions(options);
timeline.setGroups(groups);
timeline.setItems(items);
</script>
</body>
</html>

+ 7
- 0
src/component/css/groupset.css View File

@ -0,0 +1,7 @@
.graph .groupset {
position: absolute;
padding: 0;
margin: 0;
overflow: hidden;
}

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

@ -1,21 +1,4 @@
.graph .itemset {
position: absolute;
padding: 0;
margin: 0;
overflow: hidden;
}
.graph .background {
}
.graph .foreground {
}
.graph .itemset-axis {
position: absolute;
}
.graph .item {
position: absolute;
color: #1A1A1A;

+ 28
- 0
src/component/css/itemset.css View File

@ -0,0 +1,28 @@
.graph .itemset {
position: absolute;
padding: 0;
margin: 0;
overflow: hidden;
}
.graph .groupset .itemset {
position: relative;
padding: 0;
margin: 0;
overflow: hidden;
}
.graph .background {
}
.graph .foreground {
}
.graph .itemset-axis {
position: absolute;
}
.graph .groupset .itemset-axis {
position: relative;
}

+ 389
- 0
src/component/groupset.js View File

@ -0,0 +1,389 @@
/**
* An GroupSet holds a set of groups
* @param {Component} parent
* @param {Component[]} [depends] Components on which this components depends
* (except for the parent)
* @param {Object} [options] See GroupSet.setOptions for the available
* options.
* @constructor GroupSet
* @extends Panel
*/
function GroupSet(parent, depends, options) {
this.id = util.randomUUID();
this.parent = parent;
this.depends = depends;
this.options = {};
this.range = null; // Range or Object {start: number, end: number}
this.items = null; // dataset with items
this.groups = null; // dataset with groups
this.contents = {}; // object with an ItemSet for every group
// changes in groups are queued
this.queue = {};
var me = this;
this.listeners = {
'add': function (event, params) {
me._onAdd(params.items);
},
'update': function (event, params) {
me._onUpdate(params.items);
},
'remove': function (event, params) {
me._onRemove(params.items);
}
};
if (options) {
this.setOptions(options);
}
}
GroupSet.prototype = new Panel();
/**
* Set options for the ItemSet. Existing options will be extended/overwritten.
* @param {Object} [options] The following options are available:
* TODO: describe options
*/
GroupSet.prototype.setOptions = function setOptions(options) {
util.extend(this.options, options);
// TODO: implement options
};
GroupSet.prototype.setRange = function (range) {
// TODO: implement setRange
};
/**
* Set items
* @param {vis.DataSet | null} items
*/
GroupSet.prototype.setItems = function setItems(items) {
this.items = items;
util.forEach(this.contents, function (group) {
group.setItems(items);
});
};
/**
* Get items
* @return {vis.DataSet | null} items
*/
GroupSet.prototype.getItems = function getItems() {
return this.items;
};
/**
* Set range (start and end).
* @param {Range | Object} range A Range or an object containing start and end.
*/
GroupSet.prototype.setRange = function setRange(range) {
this.range = range;
};
/**
* Set groups
* @param {vis.DataSet} groups
*/
GroupSet.prototype.setGroups = function setGroups(groups) {
var me = this,
dataGroups,
ids;
// unsubscribe from current dataset
if (this.groups) {
util.forEach(this.listeners, function (callback, event) {
me.groups.unsubscribe(event, callback);
});
// remove all drawn groups
dataGroups = this.groups.get({fields: ['id']});
ids = [];
util.forEach(dataGroups, function (dataGroup, index) {
ids[index] = dataGroup.id;
});
this._onRemove(ids);
}
// replace the dataset
if (!groups) {
this.groups = null;
}
else if (groups instanceof DataSet) {
this.groups = groups;
}
else {
this.groups = new DataSet({
fieldTypes: {
start: 'Date',
end: 'Date'
}
});
this.groups.add(groups);
}
if (this.groups) {
// subscribe to new dataset
var id = this.id;
util.forEach(this.listeners, function (callback, event) {
me.groups.subscribe(event, callback, id);
});
// draw all new groups
dataGroups = this.groups.get({fields: ['id']});
ids = [];
util.forEach(dataGroups, function (dataGroup, index) {
ids[index] = dataGroup.id;
});
this._onAdd(ids);
}
};
/**
* Get groups
* @return {vis.DataSet | null} groups
*/
GroupSet.prototype.getGroups = function getGroups() {
return this.groups;
};
/**
* Repaint the component
* @return {Boolean} changed
*/
GroupSet.prototype.repaint = function repaint() {
var changed = 0,
update = util.updateProperty,
asSize = util.option.asSize,
options = this.options,
frame = this.frame;
if (!frame) {
frame = document.createElement('div');
frame.className = 'groupset';
if (options.className) {
util.addClassName(frame, util.option.asString(options.className));
}
this.frame = frame;
changed += 1;
}
if (!this.parent) {
throw new Error('Cannot repaint groupset: no parent attached');
}
var parentContainer = this.parent.getContainer();
if (!parentContainer) {
throw new Error('Cannot repaint groupset: parent has no container element');
}
if (!frame.parentNode) {
parentContainer.appendChild(frame);
changed += 1;
}
// reposition frame
changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
changed += update(frame.style, 'top', asSize(options.top, '0px'));
changed += update(frame.style, 'left', asSize(options.left, '0px'));
changed += update(frame.style, 'width', asSize(options.width, '100%'));
var me = this,
queue = this.queue,
items = this.items,
contents = this.contents,
groups = this.groups,
dataOptions = {
fields: ['id', 'content']
};
// show/hide added/changed/removed items
Object.keys(queue).forEach(function (id) {
var entry = queue[id];
var group = entry.item;
//noinspection FallthroughInSwitchStatementJS
switch (entry.action) {
case 'add':
// TODO: create group
group = new ItemSet(me);
group.setRange(me.range);
group.setItems(me.items);
// update lists
contents[id] = group;
delete queue[id];
break;
case 'update':
// TODO: update group
// update lists
contents[id] = group;
delete queue[id];
break;
case 'remove':
if (group) {
// remove DOM of the group
changed += group.hide();
}
// update lists
delete contents[id];
delete queue[id];
break;
default:
console.log('Error: unknown action "' + entry.action + '"');
}
});
// reposition all groups
util.forEach(this.contents, function (group) {
changed += group.repaint();
});
return (changed > 0);
};
/**
* Get container element
* @return {HTMLElement} container
*/
GroupSet.prototype.getContainer = function getContainer() {
// TODO: replace later on with container element for holding itemsets
return this.frame;
};
/**
* Reflow the component
* @return {Boolean} resized
*/
GroupSet.prototype.reflow = function reflow() {
var changed = 0,
options = this.options,
update = util.updateProperty,
asNumber = util.option.asNumber,
frame = this.frame;
if (frame) {
// reposition all groups
util.forEach(this.contents, function (group) {
changed += group.reflow();
});
var maxHeight = asNumber(options.maxHeight);
var height;
if (options.height != null) {
height = frame.offsetHeight;
}
else {
// height is not specified, calculate the sum of the height of all groups
height = 0;
util.forEach(this.contents, function (group) {
height += group.height;
});
}
if (maxHeight != null) {
height = Math.min(height, maxHeight);
}
changed += update(this, 'height', height);
changed += update(this, 'top', frame.offsetTop);
changed += update(this, 'left', frame.offsetLeft);
changed += update(this, 'width', frame.offsetWidth);
}
return (changed > 0);
};
/**
* Hide the component from the DOM
* @return {Boolean} changed
*/
GroupSet.prototype.hide = function hide() {
if (this.frame && this.frame.parentNode) {
this.frame.parentNode.removeChild(this.frame);
return true;
}
else {
return false;
}
};
/**
* Show the component in the DOM (when not already visible).
* A repaint will be executed when the component is not visible
* @return {Boolean} changed
*/
GroupSet.prototype.show = function show() {
if (!this.frame || !this.frame.parentNode) {
return this.repaint();
}
else {
return false;
}
};
/**
* Handle updated groups
* @param {Number[]} ids
* @private
*/
GroupSet.prototype._onUpdate = function _onUpdate(ids) {
this._toQueue(ids, 'update');
};
/**
* Handle changed groups
* @param {Number[]} ids
* @private
*/
GroupSet.prototype._onAdd = function _onAdd(ids) {
this._toQueue(ids, 'add');
};
/**
* Handle removed groups
* @param {Number[]} ids
* @private
*/
GroupSet.prototype._onRemove = function _onRemove(ids) {
this._toQueue(ids, 'remove');
};
/**
* Put groups in the queue to be added/updated/remove
* @param {Number[]} ids
* @param {String} action can be 'add', 'update', 'remove'
*/
GroupSet.prototype._toQueue = function _toQueue(ids, action) {
var groups = this.groups;
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: groups[id] || null,
action: action
};
}
});
if (this.controller) {
//this.requestReflow();
this.requestRepaint();
}
};

+ 33
- 80
src/component/itemset.js View File

@ -31,8 +31,9 @@ function ItemSet(parent, depends, options) {
this.dom = {};
var me = this;
this.data = null; // DataSet
this.items = null; // DataSet
this.range = null; // Range or Object {start: number, end: number}
this.listeners = {
'add': function (event, params) {
me._onAdd(params.items);
@ -45,8 +46,8 @@ function ItemSet(parent, depends, options) {
}
};
this.items = {};
this.queue = {}; // queue with items to be added/updated/removed
this.contents = {}; // object with an Item for every data item
this.queue = {}; // queue with id/actions: 'add', 'update', 'delete'
this.stack = new Stack(this);
this.conversion = null;
@ -179,8 +180,8 @@ ItemSet.prototype.repaint = function repaint() {
var me = this,
queue = this.queue,
data = this.data,
items = this.items,
contents = this.contents,
dataOptions = {
fields: ['id', 'start', 'end', 'content', 'type']
};
@ -189,13 +190,16 @@ ItemSet.prototype.repaint = function repaint() {
// show/hide added/changed/removed items
Object.keys(queue).forEach(function (id) {
var entry = queue[id];
var item = entry.item;
//var entry = queue[id];
var action = queue[id];
var item = contents[id];
//var item = entry.item;
//noinspection FallthroughInSwitchStatementJS
switch (entry.action) {
switch (action) {
case 'add':
case 'update':
var itemData = data.get(id, dataOptions);
var itemData = items.get(id, dataOptions);
var type = itemData.type ||
(itemData.start && itemData.end && 'range') ||
'box';
@ -227,7 +231,7 @@ ItemSet.prototype.repaint = function repaint() {
}
// update lists
items[id] = item;
contents[id] = item;
delete queue[id];
break;
@ -238,17 +242,17 @@ ItemSet.prototype.repaint = function repaint() {
}
// update lists
delete items[id];
delete contents[id];
delete queue[id];
break;
default:
console.log('Error: unknown action "' + entry.action + '"');
console.log('Error: unknown action "' + action + '"');
}
});
// reposition all items. Show items only when in the visible area
util.forEach(this.items, function (item) {
util.forEach(this.contents, function (item) {
if (item.visible) {
changed += item.show();
item.reposition();
@ -299,7 +303,7 @@ ItemSet.prototype.reflow = function reflow () {
if (frame) {
this._updateConversion();
util.forEach(this.items, function (item) {
util.forEach(this.contents, function (item) {
changed += item.reflow();
});
@ -367,15 +371,15 @@ ItemSet.prototype.hide = function hide() {
/**
* Set items
* @param {vis.DataSet | Array | DataTable | null} data
* @param {vis.DataSet | null} items
*/
ItemSet.prototype.setItems = function setItems(data) {
ItemSet.prototype.setItems = function setItems(items) {
var me = this,
dataItems,
ids;
// unsubscribe from current dataset
var current = this.data;
var current = this.items;
if (current) {
util.forEach(this.listeners, function (callback, event) {
current.unsubscribe(event, callback);
@ -391,31 +395,25 @@ ItemSet.prototype.setItems = function setItems(data) {
}
// replace the dataset
if (!data) {
this.data = null;
if (!items) {
this.items = null;
}
else if (data instanceof DataSet) {
this.data = data;
else if (items instanceof DataSet) {
this.items = items;
}
else {
this.data = new DataSet({
fieldTypes: {
start: 'Date',
end: 'Date'
}
});
this.data.add(data);
throw new TypeError('Data must be an instance of DataSet');
}
if (this.data) {
if (this.items) {
// subscribe to new dataset
var id = this.id;
util.forEach(this.listeners, function (callback, event) {
me.data.subscribe(event, callback, id);
me.items.subscribe(event, callback, id);
});
// draw all new items
dataItems = this.data.get({fields: ['id']});
dataItems = this.items.get({fields: ['id']});
ids = [];
util.forEach(dataItems, function (dataItem, index) {
ids[index] = dataItem.id;
@ -425,45 +423,11 @@ ItemSet.prototype.setItems = function setItems(data) {
};
/**
* Get the current items data
* @returns {vis.DataSet}
* Get the current items items
* @returns {vis.DataSet | null}
*/
ItemSet.prototype.getItems = function getItems() {
return this.data;
};
/**
* Get the data range of the item set.
* @returns {{min: Date, max: Date}} range A range with a start and end Date.
* When no minimum is found, min==null
* When no maximum is found, max==null
*/
ItemSet.prototype.getItemRange = function getItemRange() {
// calculate min from start filed
var data = this.data;
var minItem = data.min('start');
var min = minItem ? minItem.start.valueOf() : null;
// calculate max of both start and end fields
var max = null;
var maxStartItem = data.max('start');
if (maxStartItem) {
max = maxStartItem.start.valueOf();
}
var maxEndItem = data.max('end');
if (maxEndItem) {
if (max == null) {
max = maxEndItem.end.valueOf();
}
else {
max = Math.max(max, maxEndItem.end.valueOf());
}
}
return {
min: (min != null) ? new Date(min) : null,
max: (max != null) ? new Date(max) : null
};
return this.items;
};
/**
@ -472,6 +436,7 @@ ItemSet.prototype.getItemRange = function getItemRange() {
* @private
*/
ItemSet.prototype._onUpdate = function _onUpdate(ids) {
console.log('onUpdate', ids)
this._toQueue(ids, 'update');
};
@ -499,21 +464,9 @@ ItemSet.prototype._onRemove = function _onRemove(ids) {
* @param {String} action can be 'add', 'update', 'remove'
*/
ItemSet.prototype._toQueue = function _toQueue(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
};
}
queue[id] = action;
});
if (this.controller) {

+ 1
- 1
src/stack.js View File

@ -67,7 +67,7 @@ Stack.prototype.update = function update() {
* @private
*/
Stack.prototype._order = function _order () {
var items = this.parent.items;
var items = this.parent.contents;
if (!items) {
throw new Error('Cannot stack items: parent does not contain items');
}

+ 121
- 45
src/visualization/timeline.js View File

@ -55,16 +55,11 @@ function Timeline (container, items, options) {
this.timeaxis.setRange(this.range);
this.controller.add(this.timeaxis);
// contents panel containing the items.
// Is an ItemSet by default, can be changed to a GroupSet
this.content = new ItemSet(this.main, [this.timeaxis], {
orientation: this.options.orientation
});
this.content.setRange(this.range);
this.controller.add(this.content);
// create itemset or groupset
this.setGroups(null);
this.items = null; // data
this.groups = null; // data
this.items = null; // DataSet
this.groups = null; // DataSet
// set options (must take place before setting the data)
if (options) {
@ -108,9 +103,9 @@ Timeline.prototype.setOptions = function (options) {
}
}
if (options.height) {
if (this.options.height) {
// fixed height
mainHeight = options.height;
mainHeight = this.options.height;
itemsHeight = function () {
return me.main.height - me.timeaxis.height;
};
@ -149,44 +144,58 @@ Timeline.prototype.setOptions = function (options) {
/**
* Set items
* @param {vis.DataSet | Array | DataTable} items
* @param {vis.DataSet | Array | DataTable | null} items
*/
Timeline.prototype.setItems = function(items) {
var current = this.content.getItems();
if (!current) {
// initial load of data
this.content.setItems(items);
if (this.options.start == undefined || this.options.end == undefined) {
// apply the data range as range
var dataRange = this.content.getItemRange();
// add 5% on both sides
var min = dataRange.min;
var max = dataRange.max;
if (min != null && max != null) {
var interval = (max.valueOf() - min.valueOf());
min = new Date(min.valueOf() - interval * 0.05);
max = new Date(max.valueOf() + interval * 0.05);
}
var initialLoad = (this.items == null);
// override specified start and/or end date
if (this.options.start != undefined) {
min = new Date(this.options.start.valueOf());
}
if (this.options.end != undefined) {
max = new Date(this.options.end.valueOf());
// convert to type DataSet when needed
var newItemSet;
if (!items) {
newItemSet = null;
}
else if (items instanceof DataSet) {
newItemSet = items;
}
if (!(items instanceof DataSet)) {
newItemSet = new DataSet({
fieldTypes: {
start: 'Date',
end: 'Date'
}
});
newItemSet.add(items);
}
// apply range if there is a min or max available
if (min != null || max != null) {
this.range.setRange(min, max);
}
// set items
this.items = newItemSet;
this.content.setItems(newItemSet);
if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
// apply the data range as range
var dataRange = this.getItemRange();
// add 5% on both sides
var min = dataRange.min;
var max = dataRange.max;
if (min != null && max != null) {
var interval = (max.valueOf() - min.valueOf());
min = new Date(min.valueOf() - interval * 0.05);
max = new Date(max.valueOf() + interval * 0.05);
}
// override specified start and/or end date
if (this.options.start != undefined) {
min = new Date(this.options.start.valueOf());
}
if (this.options.end != undefined) {
max = new Date(this.options.end.valueOf());
}
// apply range if there is a min or max available
if (min != null || max != null) {
this.range.setRange(min, max);
}
}
else {
// updated data
this.content.setItems(items);
}
};
@ -195,7 +204,74 @@ Timeline.prototype.setItems = function(items) {
* @param {vis.DataSet | Array | DataTable} groups
*/
Timeline.prototype.setGroups = function(groups) {
// TODO: cleanup previous groups or itemset
this.groups = groups;
// switch content type between ItemSet or GroupSet when needed
var type = this.groups ? GroupSet : ItemSet;
if (!(this.content instanceof type)) {
// remove old content set
if (this.content) {
this.content.hide();
if (this.content.setItems) {
this.content.setItems();
}
if (this.content.setGroups) {
this.content.setGroups();
}
this.controller.remove(this.content);
}
// create new content set
this.content = new type(this.main, [this.timeaxis]);
if (this.content.setRange) {
this.content.setRange(this.range);
}
if (this.content.setItems) {
this.content.setItems(this.items);
}
if (this.content.setGroups) {
this.content.setGroups(this.groups);
}
this.controller.add(this.content);
this.setOptions(this.options);
}
};
/**
* Get the data range of the item set.
* @returns {{min: Date, max: Date}} range A range with a start and end Date.
* When no minimum is found, min==null
* When no maximum is found, max==null
*/
Timeline.prototype.getItemRange = function getItemRange() {
// calculate min from start filed
var items = this.items,
min = null,
max = null;
if (items) {
// calculate the minimum value of the field 'start'
var minItem = items.min('start');
min = minItem ? minItem.start.valueOf() : null;
// calculate maximum value of fields 'start' and 'end'
var maxStartItem = items.max('start');
if (maxStartItem) {
max = maxStartItem.start.valueOf();
}
var maxEndItem = items.max('end');
if (maxEndItem) {
if (max == null) {
max = maxEndItem.end.valueOf();
}
else {
max = Math.max(max, maxEndItem.end.valueOf());
}
}
}
return {
min: (min != null) ? new Date(min) : null,
max: (max != null) ? new Date(max) : null
};
};

+ 662
- 137
vis.js
File diff suppressed because it is too large
View File


+ 4
- 4
vis.min.js
File diff suppressed because it is too large
View File


Loading…
Cancel
Save