Browse Source

DataView and grouping in action

css_transitions
josdejong 11 years ago
parent
commit
2ebc747903
10 changed files with 598 additions and 294 deletions
  1. +10
    -13
      src/component/groupset.js
  2. +1
    -1
      src/component/item/itembox.js
  3. +51
    -41
      src/component/itemset.js
  4. +41
    -0
      src/dataset.js
  5. +85
    -15
      src/dataview.js
  6. +6
    -2
      test/dataset.html
  7. +8
    -0
      test/dataview.js
  8. +8
    -7
      test/timeline.html
  9. +384
    -211
      vis.js
  10. +4
    -4
      vis.min.js

+ 10
- 13
src/component/groupset.js View File

@ -72,7 +72,7 @@ GroupSet.prototype.setItems = function setItems(items) {
this.items = items; this.items = items;
util.forEach(this.contents, function (group) { util.forEach(this.contents, function (group) {
group.itemset.setItems(items);
group.view.setData(items);
}); });
}; };
@ -229,31 +229,28 @@ GroupSet.prototype.repaint = function repaint() {
switch (action) { switch (action) {
case 'add': case 'add':
case 'update': case 'update':
// group does not yet exist
if (!group) { if (!group) {
// group does not yet exist, create a group
var itemset = new ItemSet(me); var itemset = new ItemSet(me);
itemset.setOptions(util.extend({
top: function () {
return 0; // TODO
}
}, me.options));
itemset.setOptions(me.options);
itemset.setRange(me.range); itemset.setRange(me.range);
itemset.setItems(me.items);
/* TODO: create a DataView for every group
itemset.setItems(new DataView(me.items, {filter: function (item) {
var view = new DataView(me.items, {filter: function (item) {
return item.group == id; return item.group == id;
}}));
*/
}});
itemset.setItems(view);
me.controller.add(itemset); me.controller.add(itemset);
group = { group = {
id: id, id: id,
data: groups.get(id),
view: view,
itemset: itemset itemset: itemset
}; };
contents.push(group); contents.push(group);
} }
// update group data
group.data = groups.get(id);
delete queue[id]; delete queue[id];
break; break;

+ 1
- 1
src/component/item/itembox.js View File

@ -202,7 +202,7 @@ ItemBox.prototype.reflow = function reflow() {
options = this.options; options = this.options;
start = this.parent.toScreen(this.data.start); start = this.parent.toScreen(this.data.start);
align = options && options.align; align = options && options.align;
orientation = options.orientation;
orientation = options && options.orientation;
changed += update(props.dot, 'height', dom.dot.offsetHeight); changed += update(props.dot, 'height', dom.dot.offsetHeight);
changed += update(props.dot, 'width', dom.dot.offsetWidth); changed += update(props.dot, 'width', dom.dot.offsetWidth);

+ 51
- 41
src/component/itemset.js View File

@ -181,8 +181,13 @@ ItemSet.prototype.repaint = function repaint() {
// reposition axis // reposition axis
changed += update(this.dom.axis.style, 'left', asSize(options.left, '0px')); changed += update(this.dom.axis.style, 'left', asSize(options.left, '0px'));
changed += update(this.dom.axis.style, 'top', (this.height + this.top) + 'px');
changed += update(this.dom.axis.style, 'width', asSize(options.width, '100%')); changed += update(this.dom.axis.style, 'width', asSize(options.width, '100%'));
if (this.options.orientation == 'bottom') {
changed += update(this.dom.axis.style, 'top', (this.height + this.top) + 'px');
}
else { // orientation == 'top'
changed += update(this.dom.axis.style, 'top', this.top + 'px');
}
this._updateConversion(); this._updateConversion();
@ -191,10 +196,9 @@ ItemSet.prototype.repaint = function repaint() {
items = this.items, items = this.items,
contents = this.contents, contents = this.contents,
dataOptions = { dataOptions = {
fields: ['id', 'start', 'end', 'content', 'type']
fields: [(items && items.fieldId || 'id'), 'start', 'end', 'content', 'type']
}; };
// TODO: copy options from the itemset itself? // TODO: copy options from the itemset itself?
// TODO: make orientation dynamically changable for the items
// show/hide added/changed/removed items // show/hide added/changed/removed items
Object.keys(queue).forEach(function (id) { Object.keys(queue).forEach(function (id) {
@ -206,40 +210,43 @@ ItemSet.prototype.repaint = function repaint() {
switch (action) { switch (action) {
case 'add': case 'add':
case 'update': case 'update':
var itemData = items.get(id, dataOptions);
var type = itemData.type ||
(itemData.start && itemData.end && 'range') ||
'box';
var constructor = ItemSet.types[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, hide and delete the item
changed += item.hide();
item = null;
}
else {
item.data = itemData; // TODO: create a method item.setData ?
changed++;
var itemData = items && items.get(id, dataOptions);
if (itemData) {
var type = itemData.type ||
(itemData.start && itemData.end && 'range') ||
'box';
var constructor = ItemSet.types[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, hide and delete the item
changed += item.hide();
item = null;
}
else {
item.data = itemData; // TODO: create a method item.setData ?
changed++;
}
} }
}
if (!item) {
// create item
if (constructor) {
item = new constructor(me, itemData, options);
changed++;
}
else {
throw new TypeError('Unknown item type "' + type + '"');
if (!item) {
// create item
if (constructor) {
item = new constructor(me, itemData, options);
changed++;
}
else {
throw new TypeError('Unknown item type "' + type + '"');
}
} }
contents[id] = item;
} }
// update lists
contents[id] = item;
// update queue
delete queue[id]; delete queue[id];
break; break;
@ -384,6 +391,7 @@ ItemSet.prototype.hide = function hide() {
ItemSet.prototype.setItems = function setItems(items) { ItemSet.prototype.setItems = function setItems(items) {
var me = this, var me = this,
dataItems, dataItems,
fieldId,
ids; ids;
// unsubscribe from current dataset // unsubscribe from current dataset
@ -394,10 +402,11 @@ ItemSet.prototype.setItems = function setItems(items) {
}); });
// remove all drawn items // remove all drawn items
dataItems = current.get({fields: ['id']});
fieldId = this.items.fieldId;
dataItems = current.get({fields: [fieldId]});
ids = []; ids = [];
util.forEach(dataItems, function (dataItem, index) { util.forEach(dataItems, function (dataItem, index) {
ids[index] = dataItem.id;
ids[index] = dataItem[fieldId];
}); });
this._onRemove(ids); this._onRemove(ids);
} }
@ -421,10 +430,11 @@ ItemSet.prototype.setItems = function setItems(items) {
}); });
// draw all new items // draw all new items
dataItems = this.items.get({fields: ['id']});
fieldId = this.items.fieldId;
dataItems = this.items.get({fields: [fieldId]});
ids = []; ids = [];
util.forEach(dataItems, function (dataItem, index) { util.forEach(dataItems, function (dataItem, index) {
ids[index] = dataItem.id;
ids[index] = dataItem[fieldId];
}); });
this._onAdd(ids); this._onAdd(ids);
} }
@ -444,7 +454,7 @@ ItemSet.prototype.getItems = function getItems() {
* @private * @private
*/ */
ItemSet.prototype._onUpdate = function _onUpdate(ids) { ItemSet.prototype._onUpdate = function _onUpdate(ids) {
this._toQueue(ids, 'update');
this._toQueue('update', ids);
}; };
/** /**
@ -453,7 +463,7 @@ ItemSet.prototype._onUpdate = function _onUpdate(ids) {
* @private * @private
*/ */
ItemSet.prototype._onAdd = function _onAdd(ids) { ItemSet.prototype._onAdd = function _onAdd(ids) {
this._toQueue(ids, 'add');
this._toQueue('add', ids);
}; };
/** /**
@ -462,15 +472,15 @@ ItemSet.prototype._onAdd = function _onAdd(ids) {
* @private * @private
*/ */
ItemSet.prototype._onRemove = function _onRemove(ids) { ItemSet.prototype._onRemove = function _onRemove(ids) {
this._toQueue(ids, 'remove');
this._toQueue('remove', ids);
}; };
/** /**
* Put items in the queue to be added/updated/remove * Put items in the queue to be added/updated/remove
* @param {Number[]} ids
* @param {String} action can be 'add', 'update', 'remove' * @param {String} action can be 'add', 'update', 'remove'
* @param {Number[]} ids
*/ */
ItemSet.prototype._toQueue = function _toQueue(ids, action) {
ItemSet.prototype._toQueue = function _toQueue(action, ids) {
var queue = this.queue; var queue = this.queue;
ids.forEach(function (id) { ids.forEach(function (id) {
queue[id] = action; queue[id] = action;

+ 41
- 0
src/dataset.js View File

@ -384,6 +384,46 @@ DataSet.prototype.get = function (args) {
return data; return data;
}; };
/**
* Get ids of all items or from a filtered set of items.
* @param {Object} [options] An Object with options. Available options:
* {function} [filter] filter items
* TODO: implement an option order
* @return {Array} ids
*/
DataSet.prototype.getIds = function (options) {
var data = this.data,
id,
item,
ids = [];
if (options && options.filter) {
// get filtered items
var itemOptions = this._composeItemOptions({
filter: options && options.filter
});
for (id in data) {
if (data.hasOwnProperty(id)) {
item = this._getItem(id, itemOptions);
if (item) {
ids.push(item[this.fieldId]);
}
}
}
}
else {
// get all items
for (id in data) {
if (data.hasOwnProperty(id)) {
item = data[id];
ids.push(item[this.fieldId]);
}
}
}
return ids;
};
/** /**
* Execute a callback function for every item in the dataset. * Execute a callback function for every item in the dataset.
* The order of the items is not determined. * The order of the items is not determined.
@ -392,6 +432,7 @@ DataSet.prototype.get = function (args) {
* {Object.<String, String>} [fieldTypes] * {Object.<String, String>} [fieldTypes]
* {String[]} [fields] filter fields * {String[]} [fields] filter fields
* {function} [filter] filter items * {function} [filter] filter items
* TODO: implement an option order
*/ */
DataSet.prototype.forEach = function (callback, options) { DataSet.prototype.forEach = function (callback, options) {
var itemOptions = this._composeItemOptions(options), var itemOptions = this._composeItemOptions(options),

+ 85
- 15
src/dataview.js View File

@ -10,17 +10,15 @@
*/ */
function DataView (data, options) { function DataView (data, options) {
this.data = null; this.data = null;
this.options = options || {};
this.fieldId = 'id'; // name of the field containing id
this.subscribers = {}; // event subscribers
var me = this; var me = this;
this.listener = function () { this.listener = function () {
me._onEvent.apply(me, arguments); me._onEvent.apply(me, arguments);
}; };
this.options = options || {};
// event subscribers
this.subscribers = {};
this.setData(data); this.setData(data);
} }
@ -29,16 +27,43 @@ function DataView (data, options) {
* @param {DataSet | DataView} data * @param {DataSet | DataView} data
*/ */
DataView.prototype.setData = function (data) { DataView.prototype.setData = function (data) {
// unsubscribe from current dataset
if (this.data && this.data.unsubscribe) {
this.data.unsubscribe('*', this.listener);
var ids, dataItems, i, len;
if (this.data) {
// unsubscribe from current dataset
if (this.data.unsubscribe) {
this.data.unsubscribe('*', this.listener);
}
// trigger a remove of all drawn items
dataItems = this.get({fields: [this.fieldId, 'group']});
ids = [];
for (i = 0, len = dataItems.length; i < len; i++) {
ids[i] = dataItems[i].id;
}
this._trigger('remove', {items: ids});
} }
this.data = data; this.data = data;
// subscribe to new dataset
if (this.data && this.data.subscribe) {
this.data.subscribe('*', this.listener);
if (this.data) {
// update fieldId
this.fieldId = this.options.fieldId ||
(this.data && this.data.options && this.data.options.fieldId) ||
'id';
// trigger an add of all added items
dataItems = this.get({fields: [this.fieldId, 'group']});
ids = [];
for (i = 0, len = dataItems.length; i < len; i++) {
ids[i] = dataItems[i].id;
}
this._trigger('add', {items: ids});
// subscribe to new dataset
if (this.data.subscribe) {
this.data.subscribe('*', this.listener);
}
} }
}; };
@ -110,7 +135,47 @@ DataView.prototype.get = function (args) {
} }
getArguments.push(viewOptions); getArguments.push(viewOptions);
getArguments.push(data); getArguments.push(data);
return this.data.get.apply(this.data, getArguments);
return this.data && this.data.get.apply(this.data, getArguments);
};
/**
* Get ids of all items or from a filtered set of items.
* @param {Object} [options] An Object with options. Available options:
* {function} [filter] filter items
* TODO: implement an option order
* @return {Array} ids
*/
DataView.prototype.getIds = function (options) {
var ids;
if (this.data) {
var defaultFilter = this.options.filter;
var filter;
if (options && options.filter) {
if (defaultFilter) {
filter = function (item) {
return defaultFilter(item) && options.filter(item);
}
}
else {
filter = options.filter;
}
}
else {
filter = defaultFilter;
}
ids = this.data.getIds({
filter: filter
});
}
else {
ids = [];
}
return ids;
}; };
/** /**
@ -125,8 +190,7 @@ DataView.prototype.get = function (args) {
DataView.prototype._onEvent = function (event, params, senderId) { DataView.prototype._onEvent = function (event, params, senderId) {
var items = params && params.items, var items = params && params.items,
data = this.data, data = this.data,
fieldId = this.options.fieldId ||
(this.data && this.data.options && this.data.options.fieldId) || 'id',
fieldId = this.fieldId,
filter = this.options.filter, filter = this.options.filter,
filteredItems = []; filteredItems = [];
@ -134,9 +198,15 @@ DataView.prototype._onEvent = function (event, params, senderId) {
filteredItems = data.get(items, { filteredItems = data.get(items, {
filter: filter filter: filter
}).map(function (item) { }).map(function (item) {
return item.id;
return item[fieldId];
}); });
// TODO: dataview must trigger events from its own point of view:
// a changed item can be:
// - added to the filtered set
// - removed from the filtered set
// - changed in the filtered set
if (filteredItems.length) { if (filteredItems.length) {
this._trigger(event, {items: filteredItems}, senderId); this._trigger(event, {items: filteredItems}, senderId);
} }

+ 6
- 2
test/dataset.html View File

@ -16,13 +16,17 @@
// create an anonymous event listener // create an anonymous event listener
dataset.subscribe('*', function (event, params, id) { dataset.subscribe('*', function (event, params, id) {
console.log('anonymous listener ', event, params, id);
if (id != undefined) {
console.log('anonymous listener ', event, params, id);
}
}); });
// create a named event listener // create a named event listener
var entityId = '123'; var entityId = '123';
dataset.subscribe('*', function (event, params, id) { dataset.subscribe('*', function (event, params, id) {
console.log('named listener ', event, params, id);
if (id != entityId) {
console.log('named listener ', event, params, id);
}
}, entityId); }, entityId);
// anonymous put // anonymous put

+ 8
- 0
test/dataview.js View File

@ -55,3 +55,11 @@ groups.update({id:5, content: 'Item 5 (changed)'});
assert.equal(groupsTriggerCount, 2); assert.equal(groupsTriggerCount, 2);
assert.equal(group2TriggerCount, 1); assert.equal(group2TriggerCount, 1);
// detach the view from groups
group2.setData(null);
assert.equal(groupsTriggerCount, 2);
assert.equal(group2TriggerCount, 2);
groups.update({id:2, content: 'Item 2 (changed again)'});
assert.equal(groupsTriggerCount, 3);
assert.equal(group2TriggerCount, 2);

+ 8
- 7
test/timeline.html View File

@ -44,17 +44,18 @@
fieldTypes: { fieldTypes: {
start: 'Date', start: 'Date',
end: 'Date' end: 'Date'
}
},
fieldId: '_id'
}); });
data.add([ 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',
{_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(), start: now.clone().add('days', 0).toDate(),
end: now.clone().add('days', 7).toDate()}, end: now.clone().add('days', 7).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()}
{_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 container = document.getElementById('visualization');

+ 384
- 211
vis.js View File

@ -5,7 +5,7 @@
* A dynamic, browser-based visualization library. * A dynamic, browser-based visualization library.
* *
* @version 0.0.8 * @version 0.0.8
* @date 2013-05-17
* @date 2013-05-21
* *
* @license * @license
* Copyright (C) 2011-2013 Almende B.V, http://almende.com * Copyright (C) 2011-2013 Almende B.V, http://almende.com
@ -1525,14 +1525,17 @@ function DataSet (options) {
this.fieldTypes = {}; // field types by field name this.fieldTypes = {}; // field types by field name
if (this.options.fieldTypes) { 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;
for (var field in this.options.fieldTypes) {
if (this.options.fieldTypes.hasOwnProperty(field)) {
var value = this.options.fieldTypes[field];
if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') {
this.fieldTypes[field] = 'Date';
}
else {
this.fieldTypes[field] = value;
}
} }
});
}
} }
// event subscribers // event subscribers
@ -1599,11 +1602,12 @@ DataSet.prototype._trigger = function (event, params, senderId) {
subscribers = subscribers.concat(this.subscribers['*']); subscribers = subscribers.concat(this.subscribers['*']);
} }
subscribers.forEach(function (listener) {
if (listener.callback) {
listener.callback(event, params, senderId || null);
for (var i = 0; i < subscribers.length; i++) {
var subscriber = subscribers[i];
if (subscriber.callback) {
subscriber.callback(event, params, senderId || null);
} }
});
}
}; };
/** /**
@ -1619,19 +1623,21 @@ DataSet.prototype.add = function (data, senderId) {
if (data instanceof Array) { if (data instanceof Array) {
// Array // Array
data.forEach(function (item) {
var id = me._addItem(item);
for (var i = 0, len = data.length; i < len; i++) {
id = me._addItem(data[i]);
addedItems.push(id); addedItems.push(id);
});
}
} }
else if (util.isDataTable(data)) { else if (util.isDataTable(data)) {
// Google DataTable // Google DataTable
var columns = this._getColumnNames(data); var columns = this._getColumnNames(data);
for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) { for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
var item = {}; var item = {};
columns.forEach(function (field, col) {
for (var col = 0, cols = columns.length; col < cols; col++) {
var field = columns[col];
item[field] = data.getValue(row, col); item[field] = data.getValue(row, col);
});
}
id = me._addItem(item); id = me._addItem(item);
addedItems.push(id); addedItems.push(id);
} }
@ -1677,18 +1683,20 @@ DataSet.prototype.update = function (data, senderId) {
if (data instanceof Array) { if (data instanceof Array) {
// Array // Array
data.forEach(function (item) {
addOrUpdate(item);
});
for (var i = 0, len = data.length; i < len; i++) {
addOrUpdate(data[i]);
}
} }
else if (util.isDataTable(data)) { else if (util.isDataTable(data)) {
// Google DataTable // Google DataTable
var columns = this._getColumnNames(data); var columns = this._getColumnNames(data);
for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) { for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
var item = {}; var item = {};
columns.forEach(function (field, col) {
for (var col = 0, cols = columns.length; col < cols; col++) {
var field = columns[col];
item[field] = data.getValue(row, col); item[field] = data.getValue(row, col);
});
}
addOrUpdate(item); addOrUpdate(item);
} }
} }
@ -1787,36 +1795,39 @@ DataSet.prototype.get = function (args) {
type = 'Array'; type = 'Array';
} }
// get options
var fieldTypes = this._mergeFieldTypes(options && options.fieldTypes);
var fields = options && options.fields;
var filter = options && options.filter;
// build options
var itemOptions = this._composeItemOptions(options);
var item, itemId, i, len;
if (type == 'DataTable') { if (type == 'DataTable') {
// return a Google DataTable // return a Google DataTable
var columns = this._getColumnNames(data); var columns = this._getColumnNames(data);
if (id != undefined) { if (id != undefined) {
// return a single item // return a single item
var item = me._castItem(me.data[id], fieldTypes, fields);
this._appendRow(data, columns, item);
item = me._getItem(id, itemOptions);
if (item) {
this._appendRow(data, columns, item);
}
} }
else if (ids != undefined) { else if (ids != undefined) {
// return a subset of items // return a subset of items
ids.forEach(function (id) {
var castedItem = me._castItem(me.data[id], fieldTypes, fields);
if (!castedItem || filter(castedItem)) { // TODO: filter should be applied on the casted item but with all fields
me._appendRow(data, columns, castedItem);
for (i = 0, len = ids.length; i < len; i++) {
item = me._getItem(ids[i], itemOptions);
if (item) {
me._appendRow(data, columns, item);
} }
});
}
} }
else { else {
// return all items // return all items
util.forEach(this.data, function (item) {
var castedItem = me._castItem(item);
if (!castedItem || filter(castedItem)) { // TODO: filter should be applied on the casted item but with all fields
me._appendRow(data, columns, castedItem);
for (itemId in this.data) {
if (this.data.hasOwnProperty(itemId)) {
item = me._getItem(itemId, itemOptions);
if (item) {
me._appendRow(data, columns, item);
}
} }
});
}
} }
} }
else { else {
@ -1827,31 +1838,73 @@ DataSet.prototype.get = function (args) {
if (id != undefined) { if (id != undefined) {
// return a single item // return a single item
return this._castItem(me.data[id], fieldTypes, fields);
return me._getItem(id, itemOptions);
} }
else if (ids != undefined) { else if (ids != undefined) {
// return a subset of items // return a subset of items
ids.forEach(function (id) {
var castedItem = me._castItem(me.data[id], fieldTypes, fields);
if (!filter || filter(castedItem)) { // TODO: filter should be applied on the casted item but with all fields
data.push(castedItem);
for (i = 0, len = ids.length; i < len; i++) {
item = me._getItem(ids[i], itemOptions);
if (item) {
data.push(item);
} }
});
}
} }
else { else {
// return all items // return all items
util.forEach(this.data, function (item) {
var castedItem = me._castItem(item, fieldTypes, fields);
if (!filter || filter(castedItem)) { // TODO: filter should be applied on the casted item but with all fields
data.push(castedItem);
for (itemId in this.data) {
if (this.data.hasOwnProperty(itemId)) {
item = me._getItem(itemId, itemOptions);
if (item) {
data.push(item);
}
} }
});
}
} }
} }
return data; return data;
}; };
/**
* Get ids of all items or from a filtered set of items.
* @param {Object} [options] An Object with options. Available options:
* {function} [filter] filter items
* TODO: implement an option order
* @return {Array} ids
*/
DataSet.prototype.getIds = function (options) {
var data = this.data,
id,
item,
ids = [];
if (options && options.filter) {
// get filtered items
var itemOptions = this._composeItemOptions({
filter: options && options.filter
});
for (id in data) {
if (data.hasOwnProperty(id)) {
item = this._getItem(id, itemOptions);
if (item) {
ids.push(item[this.fieldId]);
}
}
}
}
else {
// get all items
for (id in data) {
if (data.hasOwnProperty(id)) {
item = data[id];
ids.push(item[this.fieldId]);
}
}
}
return ids;
};
/** /**
* Execute a callback function for every item in the dataset. * Execute a callback function for every item in the dataset.
* The order of the items is not determined. * The order of the items is not determined.
@ -1860,19 +1913,21 @@ DataSet.prototype.get = function (args) {
* {Object.<String, String>} [fieldTypes] * {Object.<String, String>} [fieldTypes]
* {String[]} [fields] filter fields * {String[]} [fields] filter fields
* {function} [filter] filter items * {function} [filter] filter items
* TODO: implement an option order
*/ */
DataSet.prototype.forEach = function (callback, options) { DataSet.prototype.forEach = function (callback, options) {
var fieldTypes = this._mergeFieldTypes(options && options.fieldTypes);
var fields = options && options.fields;
var filter = options && options.filter;
var me = this;
var itemOptions = this._composeItemOptions(options),
data = this.data,
item;
util.forEach(this.data, function (item, id) {
var castedItem = me._castItem(item, fieldTypes, fields);
if (!filter || filter(castedItem)) {
callback(castedItem, id);
for (var id in data) {
if (data.hasOwnProperty(id)) {
item = this._getItem(id, itemOptions);
if (item) {
callback(item, id);
}
} }
});
}
}; };
/** /**
@ -1886,47 +1941,55 @@ DataSet.prototype.forEach = function (callback, options) {
* @return {Object[]} mappedItems * @return {Object[]} mappedItems
*/ */
DataSet.prototype.map = function (callback, options) { DataSet.prototype.map = function (callback, options) {
var fieldTypes = this._mergeFieldTypes(options && options.fieldTypes);
var fields = options && options.fields;
var filter = options && options.filter;
var me = this;
var mappedItems = [];
var itemOptions = this._composeItemOptions(options),
mappedItems = [],
data = this.data,
item;
util.forEach(this.data, function (item, id) {
var castedItem = me._castItem(item, fieldTypes, fields);
if (!filter || filter(castedItem)) {
var mappedItem = callback(castedItem, id);
mappedItems.push(mappedItem);
for (var id in data) {
if (data.hasOwnProperty(id)) {
item = this._getItem(id, itemOptions);
if (item) {
mappedItems.push(callback(item, id));
}
} }
});
}
return mappedItems; return mappedItems;
}; };
/** /**
* Merge the provided field types with the datasets fieldtypes
* @param {Object} fieldTypes
* @returns {Object} mergedFieldTypes
* Build an option set for getting an item. Options will be merged by the
* default options of the dataset.
* @param {Object} options
* @returns {Object} itemOptions
* @private * @private
*/ */
DataSet.prototype._mergeFieldTypes = function (fieldTypes) {
var merged = {};
DataSet.prototype._composeItemOptions = function (options) {
var itemOptions = {},
field;
// extend with the datasets fieldTypes
if (this.options && this.options.fieldTypes) {
util.forEach(this.options.fieldTypes, function (value, field) {
merged[field] = value;
});
}
if (options) {
// get the default field types
itemOptions.fieldTypes = {};
if (this.options && this.options.fieldTypes) {
util.extend(itemOptions.fieldTypes, this.options.fieldTypes);
}
// extend with provided fieldTypes
if (fieldTypes) {
util.forEach(fieldTypes, function (value, field) {
merged[field] = value;
});
// extend field types with provided types
if (options.fieldTypes) {
util.extend(itemOptions.fieldTypes, options.fieldTypes);
}
if (options.fields) {
itemOptions.fields = options.fields;
}
if (options.filter) {
itemOptions.filter = options.filter;
}
} }
return merged;
return itemOptions;
}; };
/** /**
@ -1937,7 +2000,7 @@ DataSet.prototype._mergeFieldTypes = function (fieldTypes) {
*/ */
DataSet.prototype.remove = function (id, senderId) { DataSet.prototype.remove = function (id, senderId) {
var removedItems = [], var removedItems = [],
me = this;
i, len;
if (util.isNumber(id) || util.isString(id)) { if (util.isNumber(id) || util.isString(id)) {
delete this.data[id]; delete this.data[id];
@ -1945,14 +2008,14 @@ DataSet.prototype.remove = function (id, senderId) {
removedItems.push(id); removedItems.push(id);
} }
else if (id instanceof Array) { else if (id instanceof Array) {
id.forEach(function (id) {
me.remove(id);
});
for (i = 0, len = id.length; i < len; i++) {
this.remove(id[i]);
}
removedItems = items.concat(id); removedItems = items.concat(id);
} }
else if (id instanceof Object) { else if (id instanceof Object) {
// search for the object // search for the object
for (var i in this.data) {
for (i in this.data) {
if (this.data.hasOwnProperty(i)) { if (this.data.hasOwnProperty(i)) {
if (this.data[i] == id) { if (this.data[i] == id) {
delete this.data[i]; delete this.data[i];
@ -1988,18 +2051,19 @@ DataSet.prototype.clear = function (senderId) {
*/ */
DataSet.prototype.max = function (field) { DataSet.prototype.max = function (field) {
var data = this.data, var data = this.data,
ids = Object.keys(data);
max = null,
maxField = null;
var max = null;
var maxField = null;
ids.forEach(function (id) {
var item = data[id];
var itemField = item[field];
if (itemField != null && (!max || itemField > maxField)) {
max = item;
maxField = itemField;
for (var id in data) {
if (data.hasOwnProperty(id)) {
var item = data[id];
var itemField = item[field];
if (itemField != null && (!max || itemField > maxField)) {
max = item;
maxField = itemField;
}
} }
});
}
return max; return max;
}; };
@ -2011,18 +2075,19 @@ DataSet.prototype.max = function (field) {
*/ */
DataSet.prototype.min = function (field) { DataSet.prototype.min = function (field) {
var data = this.data, var data = this.data,
ids = Object.keys(data);
min = null,
minField = null;
var min = null;
var minField = null;
ids.forEach(function (id) {
var item = data[id];
var itemField = item[field];
if (itemField != null && (!min || itemField < minField)) {
min = item;
minField = itemField;
for (var id in data) {
if (data.hasOwnProperty(id)) {
var item = data[id];
var itemField = item[field];
if (itemField != null && (!min || itemField < minField)) {
min = item;
minField = itemField;
}
} }
});
}
return min; return min;
}; };
@ -2098,44 +2163,73 @@ DataSet.prototype._addItem = function (item) {
}; };
/** /**
* Cast and filter the fields of an item
* @param {Object | undefined} item
* @param {Object.<String, String>} [fieldTypes]
* @param {String[]} [fields]
* @return {Object | null} castedItem
* Get, cast and filter an item
* @param {String} id
* @param {Object} options Available options:
* {Object.<String, String>} fieldTypes Cast field types
* {String[]} fields Filter fields
* {function} filter Filter item, returns null if
* item does not match the filter
* @return {Object | null} item
* @private * @private
*/ */
DataSet.prototype._castItem = function (item, fieldTypes, fields) {
var clone,
fieldId = this.fieldId,
internalIds = this.internalIds;
DataSet.prototype._getItem = function (id, options) {
var field, value;
if (item) {
clone = {};
fieldTypes = fieldTypes || {};
// get the item from the dataset
var raw = this.data[id];
if (!raw) {
return null;
}
if (fields) {
// output filtered fields
util.forEach(item, function (value, field) {
if (fields.indexOf(field) != -1) {
clone[field] = util.cast(value, fieldTypes[field]);
// cast the items field types
var casted = {},
fieldId = this.fieldId,
internalIds = this.internalIds;
if (options.fieldTypes) {
var fieldTypes = options.fieldTypes;
for (field in raw) {
if (raw.hasOwnProperty(field)) {
value = raw[field];
// output all fields, except internal ids
if ((field != fieldId) || !(value in internalIds)) {
casted[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 {
// no field types specified, no casting needed
for (field in raw) {
if (raw.hasOwnProperty(field)) {
value = raw[field];
// output all fields, except internal ids
if ((field != fieldId) || !(value in internalIds)) {
casted[field] = value;
} }
});
}
} }
} }
else {
clone = null;
// apply item filter
if (options.filter && !options.filter(casted)) {
return null;
} }
return clone;
// apply fields filter
if (options.fields) {
var filtered = {},
fields = options.fields;
for (field in casted) {
if (casted.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
filtered[field] = casted[field];
}
}
return filtered;
}
else {
return casted;
}
}; };
/** /**
@ -2171,7 +2265,7 @@ DataSet.prototype._updateItem = function (item) {
/** /**
* Get an array with the column names of a Google DataTable * Get an array with the column names of a Google DataTable
* @param {DataTable} dataTable * @param {DataTable} dataTable
* @return {Array} columnNames
* @return {String[]} columnNames
* @private * @private
*/ */
DataSet.prototype._getColumnNames = function (dataTable) { DataSet.prototype._getColumnNames = function (dataTable) {
@ -2191,9 +2285,11 @@ DataSet.prototype._getColumnNames = function (dataTable) {
*/ */
DataSet.prototype._appendRow = function (dataTable, columns, item) { DataSet.prototype._appendRow = function (dataTable, columns, item) {
var row = dataTable.addRow(); var row = dataTable.addRow();
columns.forEach(function (field, col) {
for (var col = 0, cols = columns.length; col < cols; col++) {
var field = columns[col];
dataTable.setValue(row, col, item[field]); dataTable.setValue(row, col, item[field]);
});
}
}; };
/** /**
@ -2208,17 +2304,15 @@ DataSet.prototype._appendRow = function (dataTable, columns, item) {
*/ */
function DataView (data, options) { function DataView (data, options) {
this.data = null; this.data = null;
this.options = options || {};
this.fieldId = 'id'; // name of the field containing id
this.subscribers = {}; // event subscribers
var me = this; var me = this;
this.listener = function () { this.listener = function () {
me._onEvent.apply(me, arguments); me._onEvent.apply(me, arguments);
}; };
this.options = options || {};
// event subscribers
this.subscribers = {};
this.setData(data); this.setData(data);
} }
@ -2227,16 +2321,43 @@ function DataView (data, options) {
* @param {DataSet | DataView} data * @param {DataSet | DataView} data
*/ */
DataView.prototype.setData = function (data) { DataView.prototype.setData = function (data) {
// unsubscribe from current dataset
if (this.data && this.data.unsubscribe) {
this.data.unsubscribe('*', this.listener);
var ids, dataItems, i, len;
if (this.data) {
// unsubscribe from current dataset
if (this.data.unsubscribe) {
this.data.unsubscribe('*', this.listener);
}
// trigger a remove of all drawn items
dataItems = this.get({fields: [this.fieldId, 'group']});
ids = [];
for (i = 0, len = dataItems.length; i < len; i++) {
ids[i] = dataItems[i].id;
}
this._trigger('remove', {items: ids});
} }
this.data = data; this.data = data;
// subscribe to new dataset
if (this.data && this.data.subscribe) {
this.data.subscribe('*', this.listener);
if (this.data) {
// update fieldId
this.fieldId = this.options.fieldId ||
(this.data && this.data.options && this.data.options.fieldId) ||
'id';
// trigger an add of all added items
dataItems = this.get({fields: [this.fieldId, 'group']});
ids = [];
for (i = 0, len = dataItems.length; i < len; i++) {
ids[i] = dataItems[i].id;
}
this._trigger('add', {items: ids});
// subscribe to new dataset
if (this.data.subscribe) {
this.data.subscribe('*', this.listener);
}
} }
}; };
@ -2308,7 +2429,47 @@ DataView.prototype.get = function (args) {
} }
getArguments.push(viewOptions); getArguments.push(viewOptions);
getArguments.push(data); getArguments.push(data);
return this.data.get.apply(this.data, getArguments);
return this.data && this.data.get.apply(this.data, getArguments);
};
/**
* Get ids of all items or from a filtered set of items.
* @param {Object} [options] An Object with options. Available options:
* {function} [filter] filter items
* TODO: implement an option order
* @return {Array} ids
*/
DataView.prototype.getIds = function (options) {
var ids;
if (this.data) {
var defaultFilter = this.options.filter;
var filter;
if (options && options.filter) {
if (defaultFilter) {
filter = function (item) {
return defaultFilter(item) && options.filter(item);
}
}
else {
filter = options.filter;
}
}
else {
filter = defaultFilter;
}
ids = this.data.getIds({
filter: filter
});
}
else {
ids = [];
}
return ids;
}; };
/** /**
@ -2323,8 +2484,7 @@ DataView.prototype.get = function (args) {
DataView.prototype._onEvent = function (event, params, senderId) { DataView.prototype._onEvent = function (event, params, senderId) {
var items = params && params.items, var items = params && params.items,
data = this.data, data = this.data,
fieldId = this.options.fieldId ||
(this.data && this.data.options && this.data.options.fieldId) || 'id',
fieldId = this.fieldId,
filter = this.options.filter, filter = this.options.filter,
filteredItems = []; filteredItems = [];
@ -2332,9 +2492,15 @@ DataView.prototype._onEvent = function (event, params, senderId) {
filteredItems = data.get(items, { filteredItems = data.get(items, {
filter: filter filter: filter
}).map(function (item) { }).map(function (item) {
return item.id;
return item[fieldId];
}); });
// TODO: dataview must trigger events from its own point of view:
// a changed item can be:
// - added to the filtered set
// - removed from the filtered set
// - changed in the filtered set
if (filteredItems.length) { if (filteredItems.length) {
this._trigger(event, {items: filteredItems}, senderId); this._trigger(event, {items: filteredItems}, senderId);
} }
@ -4459,8 +4625,13 @@ ItemSet.prototype.repaint = function repaint() {
// reposition axis // reposition axis
changed += update(this.dom.axis.style, 'left', asSize(options.left, '0px')); changed += update(this.dom.axis.style, 'left', asSize(options.left, '0px'));
changed += update(this.dom.axis.style, 'top', (this.height + this.top) + 'px');
changed += update(this.dom.axis.style, 'width', asSize(options.width, '100%')); changed += update(this.dom.axis.style, 'width', asSize(options.width, '100%'));
if (this.options.orientation == 'bottom') {
changed += update(this.dom.axis.style, 'top', (this.height + this.top) + 'px');
}
else { // orientation == 'top'
changed += update(this.dom.axis.style, 'top', this.top + 'px');
}
this._updateConversion(); this._updateConversion();
@ -4469,10 +4640,9 @@ ItemSet.prototype.repaint = function repaint() {
items = this.items, items = this.items,
contents = this.contents, contents = this.contents,
dataOptions = { dataOptions = {
fields: ['id', 'start', 'end', 'content', 'type']
fields: [(items && items.fieldId || 'id'), 'start', 'end', 'content', 'type']
}; };
// TODO: copy options from the itemset itself? // TODO: copy options from the itemset itself?
// TODO: make orientation dynamically changable for the items
// show/hide added/changed/removed items // show/hide added/changed/removed items
Object.keys(queue).forEach(function (id) { Object.keys(queue).forEach(function (id) {
@ -4484,40 +4654,43 @@ ItemSet.prototype.repaint = function repaint() {
switch (action) { switch (action) {
case 'add': case 'add':
case 'update': case 'update':
var itemData = items.get(id, dataOptions);
var type = itemData.type ||
(itemData.start && itemData.end && 'range') ||
'box';
var constructor = ItemSet.types[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, hide and delete the item
changed += item.hide();
item = null;
}
else {
item.data = itemData; // TODO: create a method item.setData ?
changed++;
var itemData = items && items.get(id, dataOptions);
if (itemData) {
var type = itemData.type ||
(itemData.start && itemData.end && 'range') ||
'box';
var constructor = ItemSet.types[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, hide and delete the item
changed += item.hide();
item = null;
}
else {
item.data = itemData; // TODO: create a method item.setData ?
changed++;
}
} }
}
if (!item) {
// create item
if (constructor) {
item = new constructor(me, itemData, options);
changed++;
}
else {
throw new TypeError('Unknown item type "' + type + '"');
if (!item) {
// create item
if (constructor) {
item = new constructor(me, itemData, options);
changed++;
}
else {
throw new TypeError('Unknown item type "' + type + '"');
}
} }
contents[id] = item;
} }
// update lists
contents[id] = item;
// update queue
delete queue[id]; delete queue[id];
break; break;
@ -4662,6 +4835,7 @@ ItemSet.prototype.hide = function hide() {
ItemSet.prototype.setItems = function setItems(items) { ItemSet.prototype.setItems = function setItems(items) {
var me = this, var me = this,
dataItems, dataItems,
fieldId,
ids; ids;
// unsubscribe from current dataset // unsubscribe from current dataset
@ -4672,10 +4846,11 @@ ItemSet.prototype.setItems = function setItems(items) {
}); });
// remove all drawn items // remove all drawn items
dataItems = current.get({fields: ['id']});
fieldId = this.items.fieldId;
dataItems = current.get({fields: [fieldId]});
ids = []; ids = [];
util.forEach(dataItems, function (dataItem, index) { util.forEach(dataItems, function (dataItem, index) {
ids[index] = dataItem.id;
ids[index] = dataItem[fieldId];
}); });
this._onRemove(ids); this._onRemove(ids);
} }
@ -4699,10 +4874,11 @@ ItemSet.prototype.setItems = function setItems(items) {
}); });
// draw all new items // draw all new items
dataItems = this.items.get({fields: ['id']});
fieldId = this.items.fieldId;
dataItems = this.items.get({fields: [fieldId]});
ids = []; ids = [];
util.forEach(dataItems, function (dataItem, index) { util.forEach(dataItems, function (dataItem, index) {
ids[index] = dataItem.id;
ids[index] = dataItem[fieldId];
}); });
this._onAdd(ids); this._onAdd(ids);
} }
@ -4722,7 +4898,7 @@ ItemSet.prototype.getItems = function getItems() {
* @private * @private
*/ */
ItemSet.prototype._onUpdate = function _onUpdate(ids) { ItemSet.prototype._onUpdate = function _onUpdate(ids) {
this._toQueue(ids, 'update');
this._toQueue('update', ids);
}; };
/** /**
@ -4731,7 +4907,7 @@ ItemSet.prototype._onUpdate = function _onUpdate(ids) {
* @private * @private
*/ */
ItemSet.prototype._onAdd = function _onAdd(ids) { ItemSet.prototype._onAdd = function _onAdd(ids) {
this._toQueue(ids, 'add');
this._toQueue('add', ids);
}; };
/** /**
@ -4740,15 +4916,15 @@ ItemSet.prototype._onAdd = function _onAdd(ids) {
* @private * @private
*/ */
ItemSet.prototype._onRemove = function _onRemove(ids) { ItemSet.prototype._onRemove = function _onRemove(ids) {
this._toQueue(ids, 'remove');
this._toQueue('remove', ids);
}; };
/** /**
* Put items in the queue to be added/updated/remove * Put items in the queue to be added/updated/remove
* @param {Number[]} ids
* @param {String} action can be 'add', 'update', 'remove' * @param {String} action can be 'add', 'update', 'remove'
* @param {Number[]} ids
*/ */
ItemSet.prototype._toQueue = function _toQueue(ids, action) {
ItemSet.prototype._toQueue = function _toQueue(action, ids) {
var queue = this.queue; var queue = this.queue;
ids.forEach(function (id) { ids.forEach(function (id) {
queue[id] = action; queue[id] = action;
@ -5080,7 +5256,7 @@ ItemBox.prototype.reflow = function reflow() {
options = this.options; options = this.options;
start = this.parent.toScreen(this.data.start); start = this.parent.toScreen(this.data.start);
align = options && options.align; align = options && options.align;
orientation = options.orientation;
orientation = options && options.orientation;
changed += update(props.dot, 'height', dom.dot.offsetHeight); changed += update(props.dot, 'height', dom.dot.offsetHeight);
changed += update(props.dot, 'width', dom.dot.offsetWidth); changed += update(props.dot, 'width', dom.dot.offsetWidth);
@ -5772,7 +5948,7 @@ GroupSet.prototype.setItems = function setItems(items) {
this.items = items; this.items = items;
util.forEach(this.contents, function (group) { util.forEach(this.contents, function (group) {
group.itemset.setItems(items);
group.view.setData(items);
}); });
}; };
@ -5929,31 +6105,28 @@ GroupSet.prototype.repaint = function repaint() {
switch (action) { switch (action) {
case 'add': case 'add':
case 'update': case 'update':
// group does not yet exist
if (!group) { if (!group) {
// group does not yet exist, create a group
var itemset = new ItemSet(me); var itemset = new ItemSet(me);
itemset.setOptions(util.extend({
top: function () {
return 0; // TODO
}
}, me.options));
itemset.setOptions(me.options);
itemset.setRange(me.range); itemset.setRange(me.range);
itemset.setItems(me.items);
/* TODO: create a DataView for every group
itemset.setItems(new DataView(me.items, {filter: function (item) {
var view = new DataView(me.items, {filter: function (item) {
return item.group == id; return item.group == id;
}}));
*/
}});
itemset.setItems(view);
me.controller.add(itemset); me.controller.add(itemset);
group = { group = {
id: id, id: id,
data: groups.get(id),
view: view,
itemset: itemset itemset: itemset
}; };
contents.push(group); contents.push(group);
} }
// update group data
group.data = groups.get(id);
delete queue[id]; delete queue[id];
break; break;

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


Loading…
Cancel
Save