Browse Source

Implemented DataView

css_transitions
josdejong 11 years ago
parent
commit
74771c66dd
12 changed files with 703 additions and 161 deletions
  1. +1
    -0
      Jakefile.js
  2. +17
    -19
      examples/timeline/05_groups.html
  3. +5
    -0
      src/component/groupset.js
  4. +13
    -7
      src/component/itemset.js
  5. +4
    -4
      src/dataset.js
  6. +149
    -0
      src/dataview.js
  7. +1
    -0
      src/module/exports.js
  8. +9
    -5
      src/util.js
  9. +102
    -1
      test/dataset.js
  10. +57
    -0
      test/dataview.js
  11. +341
    -121
      vis.js
  12. +4
    -4
      vis.min.js

+ 1
- 0
Jakefile.js View File

@ -50,6 +50,7 @@ task('build', {async: true}, function () {
'./src/events.js', './src/events.js',
'./src/timestep.js', './src/timestep.js',
'./src/dataset.js', './src/dataset.js',
'./src/dataview.js',
'./src/stack.js', './src/stack.js',
'./src/range.js', './src/range.js',
'./src/eventbus.js', './src/eventbus.js',

+ 17
- 19
examples/timeline/05_groups.html View File

@ -23,32 +23,30 @@
<script> <script>
var now = moment().minutes(0).seconds(0).milliseconds(0); var now = moment().minutes(0).seconds(0).milliseconds(0);
var groupCount = 3;
var itemCount = 20;
// create a dataset with items // create a dataset with items
var items = new vis.DataSet(); 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)}
]);
for (var i = 0; i < itemCount; i++) {
var start = now.clone().add('hours', Math.random() * 200);
var group = Math.floor(Math.random() * groupCount);
items.add({
id: i,
group: group,
content: 'item ' + i,
start: start,
type: 'point'
});
}
var groups = new vis.DataSet(); 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'}
]);
for (var g = 0; g < groupCount; g++) {
groups.add({id: g, content: 'Group ' + g});
}
var container = document.getElementById('visualization'); var container = document.getElementById('visualization');
var options = {
start: now.clone().add('days', -3),
end: now.clone().add('days', 7)
//height: '100%'
};
var options = {};
var timeline = new vis.Timeline(container); var timeline = new vis.Timeline(container);
timeline.setOptions(options); timeline.setOptions(options);

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

@ -239,6 +239,11 @@ GroupSet.prototype.repaint = function repaint() {
}, me.options)); }, me.options));
itemset.setRange(me.range); itemset.setRange(me.range);
itemset.setItems(me.items); itemset.setItems(me.items);
/* TODO: create a DataView for every group
itemset.setItems(new DataView(me.items, {filter: function (item) {
return item.group == id;
}}));
*/
me.controller.add(itemset); me.controller.add(itemset);
group = { group = {

+ 13
- 7
src/component/itemset.js View File

@ -35,14 +35,20 @@ function ItemSet(parent, depends, options) {
this.range = null; // Range or Object {start: number, end: number} this.range = null; // Range or Object {start: number, end: number}
this.listeners = { this.listeners = {
'add': function (event, params) {
me._onAdd(params.items);
'add': function (event, params, senderId) {
if (senderId != me.id) {
me._onAdd(params.items);
}
}, },
'update': function (event, params) {
me._onUpdate(params.items);
'update': function (event, params, senderId) {
if (senderId != me.id) {
me._onUpdate(params.items);
}
}, },
'remove': function (event, params) {
me._onRemove(params.items);
'remove': function (event, params, senderId) {
if (senderId != me.id) {
me._onRemove(params.items);
}
} }
}; };
@ -400,7 +406,7 @@ ItemSet.prototype.setItems = function setItems(items) {
if (!items) { if (!items) {
this.items = null; this.items = null;
} }
else if (items instanceof DataSet) {
else if (items instanceof DataSet || items instanceof DataView) {
this.items = items; this.items = items;
} }
else { else {

+ 4
- 4
src/dataset.js View File

@ -323,7 +323,7 @@ DataSet.prototype.get = function (args) {
// return a subset of items // return a subset of items
ids.forEach(function (id) { ids.forEach(function (id) {
var castedItem = me._castItem(me.data[id], fieldTypes, fields); var castedItem = me._castItem(me.data[id], fieldTypes, fields);
if (!castedItem || filter(castedItem)) {
if (!castedItem || filter(castedItem)) { // TODO: filter should be applied on the casted item but with all fields
me._appendRow(data, columns, castedItem); me._appendRow(data, columns, castedItem);
} }
}); });
@ -332,7 +332,7 @@ DataSet.prototype.get = function (args) {
// return all items // return all items
util.forEach(this.data, function (item) { util.forEach(this.data, function (item) {
var castedItem = me._castItem(item); var castedItem = me._castItem(item);
if (!castedItem || filter(castedItem)) {
if (!castedItem || filter(castedItem)) { // TODO: filter should be applied on the casted item but with all fields
me._appendRow(data, columns, castedItem); me._appendRow(data, columns, castedItem);
} }
}); });
@ -352,7 +352,7 @@ DataSet.prototype.get = function (args) {
// return a subset of items // return a subset of items
ids.forEach(function (id) { ids.forEach(function (id) {
var castedItem = me._castItem(me.data[id], fieldTypes, fields); var castedItem = me._castItem(me.data[id], fieldTypes, fields);
if (!filter || filter(castedItem)) {
if (!filter || filter(castedItem)) { // TODO: filter should be applied on the casted item but with all fields
data.push(castedItem); data.push(castedItem);
} }
}); });
@ -361,7 +361,7 @@ DataSet.prototype.get = function (args) {
// return all items // return all items
util.forEach(this.data, function (item) { util.forEach(this.data, function (item) {
var castedItem = me._castItem(item, fieldTypes, fields); var castedItem = me._castItem(item, fieldTypes, fields);
if (!filter || filter(castedItem)) {
if (!filter || filter(castedItem)) { // TODO: filter should be applied on the casted item but with all fields
data.push(castedItem); data.push(castedItem);
} }
}); });

+ 149
- 0
src/dataview.js View File

@ -0,0 +1,149 @@
/**
* DataView
*
* a dataview offers a filtered view on a dataset or an other dataview.
*
* @param {DataSet | DataView} data
* @param {Object} [options] Available options: see method get
*
* @constructor DataView
*/
function DataView (data, options) {
this.data = null;
var me = this;
this.listener = function () {
me._onEvent.apply(me, arguments);
};
this.options = options || {};
// event subscribers
this.subscribers = {};
this.setData(data);
}
/**
* Set a data source for the view
* @param {DataSet | DataView} data
*/
DataView.prototype.setData = function (data) {
// unsubscribe from current dataset
if (this.data && this.data.unsubscribe) {
this.data.unsubscribe('*', this.listener);
}
this.data = data;
// subscribe to new dataset
if (this.data && this.data.subscribe) {
this.data.subscribe('*', this.listener);
}
};
/**
* Get data from the data view
*
* Usage:
*
* get()
* get(options: Object)
* get(options: Object, data: Array | DataTable)
*
* get(id: Number)
* get(id: Number, options: Object)
* get(id: Number, options: Object, data: Array | DataTable)
*
* get(ids: Number[])
* get(ids: Number[], options: Object)
* get(ids: Number[], options: Object, data: Array | DataTable)
*
* Where:
*
* {Number | String} id The id of an item
* {Number[] | String{}} ids An array with ids of items
* {Object} options An Object with options. Available options:
* {String} [type] Type of data to be returned. Can
* be 'DataTable' or 'Array' (default)
* {Object.<String, String>} [fieldTypes]
* {String[]} [fields] field names to be returned
* {function} [filter] filter items
* TODO: implement an option order
* {Array | DataTable} [data] If provided, items will be appended to this
* array or table. Required in case of Google
* DataTable.
* @param args
*/
DataView.prototype.get = function (args) {
var me = this;
// parse the arguments
var ids, options, data;
var firstType = util.getType(arguments[0]);
if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
// get(id(s) [, options] [, data])
ids = arguments[0]; // can be a single id or an array with ids
options = arguments[1];
data = arguments[2];
}
else {
// get([, options] [, data])
options = arguments[0];
data = arguments[1];
}
// extend the options with the default options and provided options
var viewOptions = util.extend({}, this.options, options);
// create a combined filter method when needed
if (this.options.filter && options && options.filter) {
viewOptions.filter = function (item) {
return me.options.filter(item) && options.filter(item);
}
}
// build up the call to the linked data set
var getArguments = [];
if (ids != undefined) {
getArguments.push(ids);
}
getArguments.push(viewOptions);
getArguments.push(data);
return this.data.get.apply(this.data, getArguments);
};
/**
* Event listener. Will propagate all events from the connected data set to
* the subscribers of the DataView, but will filter the items and only trigger
* when there are changes in the filtered data set.
* @param {String} event
* @param {Object | null} params
* @param {String} senderId
* @private
*/
DataView.prototype._onEvent = function (event, params, senderId) {
var items = params && params.items,
data = this.data,
fieldId = this.options.fieldId ||
(this.data && this.data.options && this.data.options.fieldId) || 'id',
filter = this.options.filter,
filteredItems = [];
if (items && data && filter) {
filteredItems = data.get(items, {
filter: filter
}).map(function (item) {
return item.id;
});
if (filteredItems.length) {
this._trigger(event, {items: filteredItems}, senderId);
}
}
};
// copy subscription functionality from DataSet
DataView.prototype.subscribe = DataSet.prototype.subscribe;
DataView.prototype.unsubscribe = DataSet.prototype.unsubscribe;
DataView.prototype._trigger = DataSet.prototype._trigger;

+ 1
- 0
src/module/exports.js View File

@ -7,6 +7,7 @@ var vis = {
Controller: Controller, Controller: Controller,
DataSet: DataSet, DataSet: DataSet,
DataView: DataView,
Range: Range, Range: Range,
Stack: Stack, Stack: Stack,
TimeStep: TimeStep, TimeStep: TimeStep,

+ 9
- 5
src/util.js View File

@ -78,18 +78,22 @@ util.randomUUID = function randomUUID () {
}; };
/** /**
* Extend object a with the properties of object b.
* Extend object a with the properties of object b or a series of objects
* Only properties with defined values are copied * Only properties with defined values are copied
* @param {Object} a * @param {Object} a
* @param {Object} b
* @param {... Object} b
* @return {Object} a * @return {Object} a
*/ */
util.extend = function (a, b) { util.extend = function (a, b) {
for (var prop in b) {
if (b.hasOwnProperty(prop) && b[prop] !== undefined) {
a[prop] = b[prop];
for (var i = 1, len = arguments.length; i < len; i++) {
var other = arguments[i];
for (var prop in other) {
if (other.hasOwnProperty(prop) && other[prop] !== undefined) {
a[prop] = other[prop];
}
} }
} }
return a; return a;
}; };

+ 102
- 1
test/dataset.js View File

@ -1,3 +1,104 @@
var assert = require('assert'),
moment = require('moment'),
vis = require('../vis.js'),
DataSet = vis.DataSet;
var now = new Date();
// TODO: extensively test DataSet
var data = new DataSet({
fieldTypes: {
start: 'Date',
end: 'Date'
}
});
// add single items with different date types
data.add({id: 1, content: 'Item 1', start: new Date(now.valueOf())});
data.add({id: 2, content: 'Item 2', start: now.toISOString()});
data.add([
//{id: 3, content: 'Item 3', start: moment(now)}, // TODO: moment fails, not the same instance
{id: 3, content: 'Item 3', start: now},
{id: 4, content: 'Item 4', start: '/Date(' + now.valueOf() + ')/'}
]);
var items = data.get();
assert.equal(items.length, 4);
items.forEach(function (item) {
assert.ok(item.start instanceof Date);
});
// get filtered fields only
var sort = function (a, b) {
return a.id > b.id;
};
assert.deepEqual(data.get({
fields: ['id', 'content']
}).sort(sort), [
{id: 1, content: 'Item 1'},
{id: 2, content: 'Item 2'},
{id: 3, content: 'Item 3'},
{id: 4, content: 'Item 4'}
]);
// cast dates
assert.deepEqual(data.get({
fields: ['id', 'start'],
fieldTypes: {start: 'Number'}
}).sort(sort), [
{id: 1, start: now.valueOf()},
{id: 2, start: now.valueOf()},
{id: 3, start: now.valueOf()},
{id: 4, start: now.valueOf()}
]);
// get a single item
assert.deepEqual(data.get(1, {
fields: ['id', 'start'],
fieldTypes: {start: 'ISODate'}
}), {
id: 1,
start: now.toISOString()
});
// remove an item
data.remove(2);
assert.deepEqual(data.get({
fields: ['id']
}).sort(sort), [
{id: 1},
{id: 3},
{id: 4}
]);
// add an item
data.add({id: 5, content: 'Item 5', start: now.valueOf()});
assert.deepEqual(data.get({
fields: ['id']
}).sort(sort), [
{id: 1},
{id: 3},
{id: 4},
{id: 5}
]);
// update an item
data.update({id: 5, content: 'changed!'}); // update item (extend existing fields)
data.remove(3); // remove existing item
data.add({id: 3, other: 'bla'}); // add new item
data.update({id: 6, content: 'created!', start: now.valueOf()}); // this item is not yet existing, create it
assert.deepEqual(data.get().sort(sort), [
{id: 1, content: 'Item 1', start: now},
{id: 3, other: 'bla'},
{id: 4, content: 'Item 4', start: now},
{id: 5, content: 'changed!', start: now},
{id: 6, content: 'created!', start: now}
]);
data.clear();
assert.equal(data.get().length, 0);
// TODO: extensively test DataSet

+ 57
- 0
test/dataview.js View File

@ -0,0 +1,57 @@
var assert = require('assert'),
moment = require('moment'),
vis = require('../vis.js'),
DataSet = vis.DataSet,
DataView = vis.DataView;
var groups = new DataSet();
// add items with different groups
groups.add([
{id: 1, content: 'Item 1', group: 1},
{id: 2, content: 'Item 2', group: 2},
{id: 3, content: 'Item 3', group: 2},
{id: 4, content: 'Item 4', group: 1},
{id: 5, content: 'Item 5', group: 3}
]);
var group2 = new DataView(groups, {
filter: function (item) {
return item.group == 2;
}
});
// test getting the filtered data
assert.deepEqual(group2.get(), [
{id: 2, content: 'Item 2', group: 2},
{id: 3, content: 'Item 3', group: 2}
]);
// test filtering the view contents
assert.deepEqual(group2.get({
filter: function (item) {
return item.id > 2;
}
}), [
{id: 3, content: 'Item 3', group: 2}
]);
// test event subscription
var groupsTriggerCount = 0;
groups.subscribe('*', function () {
groupsTriggerCount++;
});
var group2TriggerCount = 0;
group2.subscribe('*', function () {
group2TriggerCount++;
});
groups.update({id:2, content: 'Item 2 (changed)'});
assert.equal(groupsTriggerCount, 1);
assert.equal(group2TriggerCount, 1);
groups.update({id:5, content: 'Item 5 (changed)'});
assert.equal(groupsTriggerCount, 2);
assert.equal(group2TriggerCount, 1);

+ 341
- 121
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-14
* @date 2013-05-17
* *
* @license * @license
* Copyright (C) 2011-2013 Almende B.V, http://almende.com * Copyright (C) 2011-2013 Almende B.V, http://almende.com
@ -109,18 +109,22 @@ util.randomUUID = function randomUUID () {
}; };
/** /**
* Extend object a with the properties of object b.
* Extend object a with the properties of object b or a series of objects
* Only properties with defined values are copied * Only properties with defined values are copied
* @param {Object} a * @param {Object} a
* @param {Object} b
* @param {... Object} b
* @return {Object} a * @return {Object} a
*/ */
util.extend = function (a, b) { util.extend = function (a, b) {
for (var prop in b) {
if (b.hasOwnProperty(prop) && b[prop] !== undefined) {
a[prop] = b[prop];
for (var i = 1, len = arguments.length; i < len; i++) {
var other = arguments[i];
for (var prop in other) {
if (other.hasOwnProperty(prop) && other[prop] !== undefined) {
a[prop] = other[prop];
}
} }
} }
return a; return a;
}; };
@ -1502,12 +1506,15 @@ TimeStep.prototype.getLabelMajor = function(date) {
* - add/remove/update data * - add/remove/update data
* - gives triggers upon changes in the data * - gives triggers upon changes in the data
* - can import/export data in various data formats * - can import/export data in various data formats
*
* @param {Object} [options] Available options: * @param {Object} [options] Available options:
* {String} fieldId Field name of the id in the * {String} fieldId Field name of the id in the
* items, 'id' by default. * items, 'id' by default.
* {Object.<String, String} fieldTypes * {Object.<String, String} fieldTypes
* A map with field names as key, * A map with field names as key,
* and the field type as value. * and the field type as value.
* TODO: implement an option for a default order
* @constructor DataSet
*/ */
function DataSet (options) { function DataSet (options) {
var me = this; var me = this;
@ -1576,9 +1583,7 @@ DataSet.prototype.unsubscribe = function (event, callback) {
* Trigger an event * Trigger an event
* @param {String} event * @param {String} event
* @param {Object | null} params * @param {Object | null} params
* @param {String} [senderId] Optional id of the sender. The event will
* be triggered for all subscribers except the
* sender itself.
* @param {String} [senderId] Optional id of the sender.
* @private * @private
*/ */
DataSet.prototype._trigger = function (event, params, senderId) { DataSet.prototype._trigger = function (event, params, senderId) {
@ -1595,20 +1600,20 @@ DataSet.prototype._trigger = function (event, params, senderId) {
} }
subscribers.forEach(function (listener) { subscribers.forEach(function (listener) {
if (listener.id != senderId && listener.callback) {
if (listener.callback) {
listener.callback(event, params, senderId || null); listener.callback(event, params, senderId || null);
} }
}); });
}; };
/** /**
* Add data. Existing items with the same id will be overwritten.
* Add data.
* Adding an item will fail when there already is an item with the same id.
* @param {Object | Array | DataTable} data * @param {Object | Array | DataTable} data
* @param {String} [senderId] Optional sender id, used to trigger events for
* all but this sender's event subscribers.
* @param {String} [senderId] Optional sender id
*/ */
DataSet.prototype.add = function (data, senderId) { DataSet.prototype.add = function (data, senderId) {
var items = [],
var addedItems = [],
id, id,
me = this; me = this;
@ -1616,7 +1621,7 @@ DataSet.prototype.add = function (data, senderId) {
// Array // Array
data.forEach(function (item) { data.forEach(function (item) {
var id = me._addItem(item); var id = me._addItem(item);
items.push(id);
addedItems.push(id);
}); });
} }
else if (util.isDataTable(data)) { else if (util.isDataTable(data)) {
@ -1628,37 +1633,52 @@ DataSet.prototype.add = function (data, senderId) {
item[field] = data.getValue(row, col); item[field] = data.getValue(row, col);
}); });
id = me._addItem(item); id = me._addItem(item);
items.push(id);
addedItems.push(id);
} }
} }
else if (data instanceof Object) { else if (data instanceof Object) {
// Single item // Single item
id = me._addItem(data); id = me._addItem(data);
items.push(id);
addedItems.push(id);
} }
else { else {
throw new Error('Unknown dataType'); throw new Error('Unknown dataType');
} }
this._trigger('add', {items: items}, senderId);
if (addedItems.length) {
this._trigger('add', {items: addedItems}, senderId);
}
}; };
/** /**
* Update existing items. Items with the same id will be merged
* Update existing items. When an item does not exist, it will be created
* @param {Object | Array | DataTable} data * @param {Object | Array | DataTable} data
* @param {String} [senderId] Optional sender id, used to trigger events for
* all but this sender's event subscribers.
* @param {String} [senderId] Optional sender id
*/ */
DataSet.prototype.update = function (data, senderId) { DataSet.prototype.update = function (data, senderId) {
var items = [],
id,
me = this;
var addedItems = [],
updatedItems = [],
me = this,
fieldId = me.fieldId;
var addOrUpdate = function (item) {
var id = item[fieldId];
if (me.data[id]) {
// update item
id = me._updateItem(item);
updatedItems.push(id);
}
else {
// add new item
id = me._addItem(item);
addedItems.push(id);
}
};
if (data instanceof Array) { if (data instanceof Array) {
// Array // Array
data.forEach(function (item) { data.forEach(function (item) {
var id = me._updateItem(item);
items.push(id);
addOrUpdate(item);
}); });
} }
else if (util.isDataTable(data)) { else if (util.isDataTable(data)) {
@ -1669,55 +1689,82 @@ DataSet.prototype.update = function (data, senderId) {
columns.forEach(function (field, col) { columns.forEach(function (field, col) {
item[field] = data.getValue(row, col); item[field] = data.getValue(row, col);
}); });
id = me._updateItem(item);
items.push(id);
addOrUpdate(item);
} }
} }
else if (data instanceof Object) { else if (data instanceof Object) {
// Single item // Single item
id = me._updateItem(data);
items.push(id);
addOrUpdate(data);
} }
else { else {
throw new Error('Unknown dataType'); throw new Error('Unknown dataType');
} }
this._trigger('update', {items: items}, senderId);
if (addedItems.length) {
this._trigger('add', {items: addedItems}, senderId);
}
if (updatedItems.length) {
this._trigger('update', {items: updatedItems}, 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
* {function} [filter] filter items
* @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
* Get a data item or multiple items.
*
* Usage:
*
* get()
* get(options: Object)
* get(options: Object, data: Array | DataTable)
*
* get(id: Number | String)
* get(id: Number | String, options: Object)
* get(id: Number | String, options: Object, data: Array | DataTable)
*
* get(ids: Number[] | String[])
* get(ids: Number[] | String[], options: Object)
* get(ids: Number[] | String[], options: Object, data: Array | DataTable)
*
* Where:
*
* {Number | String} id The id of an item
* {Number[] | String{}} ids An array with ids of items
* {Object} options An Object with options. Available options:
* {String} [type] Type of data to be returned. Can
* be 'DataTable' or 'Array' (default)
* {Object.<String, String>} [fieldTypes]
* {String[]} [fields] field names to be returned
* {function} [filter] filter items
* TODO: implement an option order
* {Array | DataTable} [data] If provided, items will be appended to this
* array or table. Required in case of Google
* DataTable.
*
* @throws Error * @throws Error
*/ */
DataSet.prototype.get = function (ids, options, data) {
DataSet.prototype.get = function (args) {
var me = this; var me = this;
// TODO: simplify handling the inputs. It's quite a mess right now...
// shift arguments when first argument contains the options
if (util.getType(ids) == 'Object') {
data = options;
options = ids;
ids = undefined;
// parse the arguments
var id, ids, options, data;
var firstType = util.getType(arguments[0]);
if (firstType == 'String' || firstType == 'Number') {
// get(id [, options] [, data])
id = arguments[0];
options = arguments[1];
data = arguments[2];
}
else if (firstType == 'Array') {
// get(ids [, options] [, data])
ids = arguments[0];
options = arguments[1];
data = arguments[2];
}
else {
// get([, options] [, data])
options = arguments[0];
data = arguments[1];
} }
var fieldTypes = this._mergeFieldTypes(options && options.fieldTypes);
var fields = options && options.fields;
var filter = options && options.filter;
// determine the return type // determine the return type
var type; var type;
@ -1740,62 +1787,65 @@ DataSet.prototype.get = function (ids, options, data) {
type = 'Array'; type = 'Array';
} }
// get options
var fieldTypes = this._mergeFieldTypes(options && options.fieldTypes);
var fields = options && options.fields;
var filter = options && options.filter;
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 (ids == undefined) {
// return all data
util.forEach(this.data, function (item) {
var castedItem = me._castItem(item);
if (!castedItem || filter(castedItem)) {
me._appendRow(data, columns, castedItem);
}
});
}
else if (util.isNumber(ids) || util.isString(ids)) {
var item = me._castItem(me.data[ids], fieldTypes, fields);
if (id != undefined) {
// return a single item
var item = me._castItem(me.data[id], fieldTypes, fields);
this._appendRow(data, columns, item); this._appendRow(data, columns, item);
} }
else if (ids instanceof Array) {
else if (ids != undefined) {
// return a subset of items
ids.forEach(function (id) { ids.forEach(function (id) {
var castedItem = me._castItem(me.data[id], fieldTypes, fields); var castedItem = me._castItem(me.data[id], fieldTypes, fields);
if (!castedItem || filter(castedItem)) {
if (!castedItem || filter(castedItem)) { // TODO: filter should be applied on the casted item but with all fields
me._appendRow(data, columns, castedItem); me._appendRow(data, columns, castedItem);
} }
}); });
} }
else { else {
throw new TypeError('Parameter "ids" must be ' +
'undefined, a String, Number, or Array');
// 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);
}
});
} }
} }
else { else {
// return an array // return an array
data = data || [];
if (ids == undefined) {
// return all data
util.forEach(this.data, function (item) {
var castedItem = me._castItem(item, fieldTypes, fields);
if (!filter || filter(castedItem)) {
data.push(castedItem);
}
});
if (!data) {
data = [];
} }
else if (util.isNumber(ids) || util.isString(ids)) {
if (id != undefined) {
// return a single item // return a single item
return this._castItem(me.data[ids], fieldTypes, fields);
return this._castItem(me.data[id], fieldTypes, fields);
} }
else if (ids instanceof Array) {
else if (ids != undefined) {
// return a subset of items
ids.forEach(function (id) { ids.forEach(function (id) {
var castedItem = me._castItem(me.data[id], fieldTypes, fields); var castedItem = me._castItem(me.data[id], fieldTypes, fields);
if (!filter || filter(castedItem)) {
if (!filter || filter(castedItem)) { // TODO: filter should be applied on the casted item but with all fields
data.push(castedItem); data.push(castedItem);
} }
}); });
} }
else { else {
throw new TypeError('Parameter "ids" must be ' +
'undefined, a String, Number, or Array');
// 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);
}
});
} }
} }
@ -1832,6 +1882,7 @@ DataSet.prototype.forEach = function (callback, options) {
* {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
* @return {Object[]} mappedItems * @return {Object[]} mappedItems
*/ */
DataSet.prototype.map = function (callback, options) { DataSet.prototype.map = function (callback, options) {
@ -1882,23 +1933,22 @@ DataSet.prototype._mergeFieldTypes = function (fieldTypes) {
* Remove an object by pointer or by id * Remove an object by pointer or by id
* @param {String | Number | Object | Array} id Object or id, or an array with * @param {String | Number | Object | Array} id Object or id, or an array with
* objects or ids to be removed * objects or ids to be removed
* @param {String} [senderId] Optional sender id, used to trigger events for
* all but this sender's event subscribers.
* @param {String} [senderId] Optional sender id
*/ */
DataSet.prototype.remove = function (id, senderId) { DataSet.prototype.remove = function (id, senderId) {
var items = [],
var removedItems = [],
me = this; me = this;
if (util.isNumber(id) || util.isString(id)) { if (util.isNumber(id) || util.isString(id)) {
delete this.data[id]; delete this.data[id];
delete this.internalIds[id]; delete this.internalIds[id];
items.push(id);
removedItems.push(id);
} }
else if (id instanceof Array) { else if (id instanceof Array) {
id.forEach(function (id) { id.forEach(function (id) {
me.remove(id); me.remove(id);
}); });
items = 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
@ -1907,19 +1957,20 @@ DataSet.prototype.remove = function (id, senderId) {
if (this.data[i] == id) { if (this.data[i] == id) {
delete this.data[i]; delete this.data[i];
delete this.internalIds[i]; delete this.internalIds[i];
items.push(i);
removedItems.push(i);
} }
} }
} }
} }
this._trigger('remove', {items: items}, senderId);
if (removedItems.length) {
this._trigger('remove', {items: removedItems}, senderId);
}
}; };
/** /**
* Clear the data * Clear the data
* @param {String} [senderId] Optional sender id, used to trigger events for
* all but this sender's event subscribers.
* @param {String} [senderId] Optional sender id
*/ */
DataSet.prototype.clear = function (senderId) { DataSet.prototype.clear = function (senderId) {
var ids = Object.keys(this.data); var ids = Object.keys(this.data);
@ -2012,18 +2063,25 @@ DataSet.prototype.distinct = function (field) {
}; };
/** /**
* Add a single item
* Add a single item. Will fail when an item with the same id already exists.
* @param {Object} item * @param {Object} item
* @return {String} id * @return {String} id
* @private * @private
*/ */
DataSet.prototype._addItem = function (item) { DataSet.prototype._addItem = function (item) {
var id = item[this.fieldId]; var id = item[this.fieldId];
if (id == undefined) {
if (id != undefined) {
// check whether this id is already taken
if (this.data[id]) {
// item already exists
throw new Error('Cannot add item: item with id ' + id + ' already exists');
}
}
else {
// generate an id // generate an id
id = util.randomUUID(); id = util.randomUUID();
item[this.fieldId] = id; item[this.fieldId] = id;
this.internalIds[id] = item; this.internalIds[id] = item;
} }
@ -2035,7 +2093,6 @@ DataSet.prototype._addItem = function (item) {
} }
} }
this.data[id] = d; this.data[id] = d;
//TODO: fail when an item with this id already exists?
return id; return id;
}; };
@ -2082,7 +2139,9 @@ DataSet.prototype._castItem = function (item, fieldTypes, fields) {
}; };
/** /**
* Update a single item: merge with existing item
* Update a single item: merge with existing item.
* Will fail when the item has no id, or when there does not exist an item
* with the same id.
* @param {Object} item * @param {Object} item
* @return {String} id * @return {String} id
* @private * @private
@ -2090,21 +2149,20 @@ DataSet.prototype._castItem = function (item, fieldTypes, fields) {
DataSet.prototype._updateItem = function (item) { DataSet.prototype._updateItem = function (item) {
var id = item[this.fieldId]; var id = item[this.fieldId];
if (id == undefined) { if (id == undefined) {
throw new Error('Item has no id (item: ' + JSON.stringify(item) + ')');
throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
} }
var d = this.data[id]; 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);
}
}
if (!d) {
// item doesn't exist
throw new Error('Cannot update item: no item with id ' + id + ' found');
} }
else {
// create new item
this._addItem(item);
// 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);
}
} }
return id; return id;
@ -2138,6 +2196,156 @@ DataSet.prototype._appendRow = function (dataTable, columns, item) {
}); });
}; };
/**
* DataView
*
* a dataview offers a filtered view on a dataset or an other dataview.
*
* @param {DataSet | DataView} data
* @param {Object} [options] Available options: see method get
*
* @constructor DataView
*/
function DataView (data, options) {
this.data = null;
var me = this;
this.listener = function () {
me._onEvent.apply(me, arguments);
};
this.options = options || {};
// event subscribers
this.subscribers = {};
this.setData(data);
}
/**
* Set a data source for the view
* @param {DataSet | DataView} data
*/
DataView.prototype.setData = function (data) {
// unsubscribe from current dataset
if (this.data && this.data.unsubscribe) {
this.data.unsubscribe('*', this.listener);
}
this.data = data;
// subscribe to new dataset
if (this.data && this.data.subscribe) {
this.data.subscribe('*', this.listener);
}
};
/**
* Get data from the data view
*
* Usage:
*
* get()
* get(options: Object)
* get(options: Object, data: Array | DataTable)
*
* get(id: Number)
* get(id: Number, options: Object)
* get(id: Number, options: Object, data: Array | DataTable)
*
* get(ids: Number[])
* get(ids: Number[], options: Object)
* get(ids: Number[], options: Object, data: Array | DataTable)
*
* Where:
*
* {Number | String} id The id of an item
* {Number[] | String{}} ids An array with ids of items
* {Object} options An Object with options. Available options:
* {String} [type] Type of data to be returned. Can
* be 'DataTable' or 'Array' (default)
* {Object.<String, String>} [fieldTypes]
* {String[]} [fields] field names to be returned
* {function} [filter] filter items
* TODO: implement an option order
* {Array | DataTable} [data] If provided, items will be appended to this
* array or table. Required in case of Google
* DataTable.
* @param args
*/
DataView.prototype.get = function (args) {
var me = this;
// parse the arguments
var ids, options, data;
var firstType = util.getType(arguments[0]);
if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
// get(id(s) [, options] [, data])
ids = arguments[0]; // can be a single id or an array with ids
options = arguments[1];
data = arguments[2];
}
else {
// get([, options] [, data])
options = arguments[0];
data = arguments[1];
}
// extend the options with the default options and provided options
var viewOptions = util.extend({}, this.options, options);
// create a combined filter method when needed
if (this.options.filter && options && options.filter) {
viewOptions.filter = function (item) {
return me.options.filter(item) && options.filter(item);
}
}
// build up the call to the linked data set
var getArguments = [];
if (ids != undefined) {
getArguments.push(ids);
}
getArguments.push(viewOptions);
getArguments.push(data);
return this.data.get.apply(this.data, getArguments);
};
/**
* Event listener. Will propagate all events from the connected data set to
* the subscribers of the DataView, but will filter the items and only trigger
* when there are changes in the filtered data set.
* @param {String} event
* @param {Object | null} params
* @param {String} senderId
* @private
*/
DataView.prototype._onEvent = function (event, params, senderId) {
var items = params && params.items,
data = this.data,
fieldId = this.options.fieldId ||
(this.data && this.data.options && this.data.options.fieldId) || 'id',
filter = this.options.filter,
filteredItems = [];
if (items && data && filter) {
filteredItems = data.get(items, {
filter: filter
}).map(function (item) {
return item.id;
});
if (filteredItems.length) {
this._trigger(event, {items: filteredItems}, senderId);
}
}
};
// copy subscription functionality from DataSet
DataView.prototype.subscribe = DataSet.prototype.subscribe;
DataView.prototype.unsubscribe = DataSet.prototype.unsubscribe;
DataView.prototype._trigger = DataSet.prototype._trigger;
/** /**
* @constructor Stack * @constructor Stack
* Stacks items on top of each other. * Stacks items on top of each other.
@ -4105,14 +4313,20 @@ function ItemSet(parent, depends, options) {
this.range = null; // Range or Object {start: number, end: number} this.range = null; // Range or Object {start: number, end: number}
this.listeners = { this.listeners = {
'add': function (event, params) {
me._onAdd(params.items);
'add': function (event, params, senderId) {
if (senderId != me.id) {
me._onAdd(params.items);
}
}, },
'update': function (event, params) {
me._onUpdate(params.items);
'update': function (event, params, senderId) {
if (senderId != me.id) {
me._onUpdate(params.items);
}
}, },
'remove': function (event, params) {
me._onRemove(params.items);
'remove': function (event, params, senderId) {
if (senderId != me.id) {
me._onRemove(params.items);
}
} }
}; };
@ -4470,7 +4684,7 @@ ItemSet.prototype.setItems = function setItems(items) {
if (!items) { if (!items) {
this.items = null; this.items = null;
} }
else if (items instanceof DataSet) {
else if (items instanceof DataSet || items instanceof DataView) {
this.items = items; this.items = items;
} }
else { else {
@ -5725,6 +5939,11 @@ GroupSet.prototype.repaint = function repaint() {
}, me.options)); }, me.options));
itemset.setRange(me.range); itemset.setRange(me.range);
itemset.setItems(me.items); itemset.setItems(me.items);
/* TODO: create a DataView for every group
itemset.setItems(new DataView(me.items, {filter: function (item) {
return item.group == id;
}}));
*/
me.controller.add(itemset); me.controller.add(itemset);
group = { group = {
@ -6201,6 +6420,7 @@ var vis = {
Controller: Controller, Controller: Controller,
DataSet: DataSet, DataSet: DataSet,
DataView: DataView,
Range: Range, Range: Range,
Stack: Stack, Stack: Stack,
TimeStep: TimeStep, TimeStep: TimeStep,

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


Loading…
Cancel
Save