/** * Data source for charts * * The chart data source expects an array of objects, example: * * <code> * [ { id: 1, label:'John', value:100 }, { id: 2, label:'Jane', value:245 }, { id: 3, label:'Martin', value:37 }, { id: 4, label:'Mary', value:99 }, { id: 5, label:'Johnny', value:127 }, { id: 6, label:'Catherine', value:55 }, { id: 7, label:'Tommy', value:18 } ] * * </code> * * Some charts(example bar charts) accepts nested data, example: * * <code> * * [ * { * "country": "United Kingdom", * "children": [ * { "name":"0-14", "people" : 5000 }, { "name":"15-64", "people" : 20000 }, { "name":"65-", "people" : 4000 } * ] * }, * { * "country": "Germany", * "children": [ * { "name":"0-14", "people" : 6000 }, { "name":"15-64", "people" : 29000 }, { "name":"65-", "people" : 4000 } * ] * } * ] * * </code> * * The chart data source will add some special properties and functions to the records. * Example: "Jane" in the data above will be something like: * <code> * { * id:'1', name: 'Jane', value, 245, * \_\_color: '#4719D2' * \_\_colorOver: '#5629E1' * \_\_count : 7, * \_\_fraction:0.35976505139500736, * \_\_sum : 681 * \_\_index: 1, * \_\_percent: 36, * \_\_angle : 0.92264101427013, * \_\_radians : 2.2604704849618193, * \_\_uid : "chart-node-iw7znu0v" * \_\_min : 18 * \_\_minAgr : 18 * \_\_max : 245 * \_\_maxAggr : 245 * \_\_parent: undefined * \_\_indexStartVal: undefined * \_\_indexFraction: undefined * \_\_indexSum: undefined * * getChildren:function() * getParent():function() * } * </code> * * where \_\_color is the records assigned color and \_\_colorOver is it's color when highlighted. * You can set this properties manually in your data. When not set, LudoJS will use colors from a color scheme. * You can also set \_\_stroke and \_\_strokeOver for stroke colors. * \_\_count is the total number or records in the array. * \_\_sum is sum(values) in the array. * \_\_fraction is record.value / record.\_\_sum * \_\_index is the index of Jane in the array(John has index 0, Jane index 1, Martin index 2 and so on). * \_\_percent is the rounded value of \_\_fraction * 100 * \_\_angle is mostly for internal use and represents this records start angle in radians when all records fill a circle. * \_\_radians is how many radians of a circle this record fills. A circle has Math.PI * 2 radians. \_\_angle and radians * are only set when values are numeric. * \_\_min is the minimum value found in the data set * \_\_max is the max value found in the data set. * \_\_maxAggr is useful for nested data sets. It is set to max(child values) in the data set. For non-nested sets, it will * have the same value as \_\_max. Example: for * * <code> * * [ * { * "country": "United Kingdom", * "children": [ * { "name":"0-14", "people" : 5000 }, { "name":"15-64", "people" : 20000 }, { "name":"65-", "people" : 4000 } * ] * }, * { * "country": "Germany", * "children": [ * { "name":"0-14", "people" : 6000 }, { "name":"15-64", "people" : 29000 }, { "name":"65-", "people" : 4000 } * ] * } * ] * * </code> * * \_\_maxAggr will be 39000(Sum children of Germany), while \_\_max will be 29000. * * \_\_parent will for child items contain a reference to parent id which can be retrieved using dataSource.byId(id) * * \_\_indexStartVal stores the sum of previous records with the same index as this one. In the example with countries above, * , the value for { "name":"0-14", "people" : 6000 } will be 5000, since the first child of United Kingdom has value 5000. * This value is used when rendering stacked area charts. * * \_\_indexFraction stores the size of this record divided by the sum of all records with the same index. * * getParent() returns a reference to parent record if set, it will return undefined otherwise. * getChildren() returns reference to child data array, example the children array of Germany in the example above * @class ludo.chart.DataSource * @param {Object} config * @param {Array} config.data Pie chart data. * @param {String} config.url Get chart data from this url. config.data will not be set when you use an url. * @param {Function} config.valueOf Function which returns value of a node. Two arguments are sent to this method: 1) the record, * 2) The View asking for the value. Example * <code> * valueOf:function(record, caller){ * return record.value; * } * </code> * @param {Function} config.textOf Function which returns text of a node. The record and caller are sent to this function. * You can return different texts based on the type attribute of the caller. example: * <code> * textOf:function(record, caller){ if(caller.type == 'chart.Tooltip'){ // return text for the tooltip return record.label + ': '+ record.value + ' (' + record.__percent + '%)'; } // Default text return record.label; } * * </code> * @param {String} config.valueKey the key in the data for value, default: 'value' * @param {Function} config.getText Function returning text. Argument to this function: The View asking for the text, example, a ludo.chart.Text * @param {Function} config.max Function returning max value for the chart. This is optional. If not set, it will return the maximum value found in the data array. * For bar charts, you might want to use this to return a higher value, example: <code>max:function(){ return this.maxVal + 20 }</code>. * @param {Function} config.min Function returning min value for the chart. Default: minimum(0, data arrays minimum value) * @param {Function} config.value. Function returning a value for display. Arguments. 1) value, 2) caller. Example for a label, you might want to return 10 instead of value 10 000 000. * @param {Function} config.increments. Function returning increments for lines, labels etc. This function may return an array of values(example: for a chart with values form 0-100, this function * may return [0,10,20,30,40,50,60,70,80,90,100]. This function may also return a numeric value, example: 10 instead of the array. Three arguments are sent to this function: 1) the data arrays * minimum value, 2) the data arrays maximum value and 3) The caller, i.e. the SVG view asking for the increments. * @param {Function } config.valueForDisplay Optional function returning value to display in a view. Arguments: 1) value, 2) caller. Let's say you have a * data source with values in millions, example: Population in countries. For the chart.ChartValues view, you might want to display number of millians, i.e. * 10 instead of 10000000. This can be done with a valueForDisplay function: * <code> * valueForDisplay:function(value, caller){ * if(caller.type == 'chart.ChartValues')return Math.round(value / 1000000) + " mill"; * return value; * } * </code> * @param {Function} config.strokeOf Optional function returning stroke color for chart item, Arguments: 1) chart record, 2) caller * @param {Function} config.strokeOverOf Optional function returning mouse over stroke color for chart item, Arguments: 1) chart record, 2) caller * @param {String} config.childKey Key for child arrays, default: "children" * @param {Function} config.shouldInheritColor Optional function returning true if color should be inherited from parent record. Input: record, 2: caller * @param {Function} config.shapeOf Optional function returning shape of a record. This is used when rendering dots for the line chart. Default shape is "circle". Can also be * "rect", "triangle" or path to an image. * @param {Number} config.minBrightness Optional minimum brightness(0-100) when setting colors. * @param {Number} config.maxBrightness Optional maximum brightness(0-100) when setting colors. * @param {Number} config.minSaturation Optional minimum saturation/color intensity(0-100) when setting colors. * @param {Number} config.maxSaturation Optional maximum saturation/color intensity(0-100) when setting colors. * @param {Function} config.indexStartValueOf Optional function returning sum value of all previous records * with same index. By default, it returns record.\_\_indexStartVal. Example for { "name":"0-14", "people" : 6000 } * above it will return 5000, since this is index 0 and the child of United Kingdom with same index has value 5000. * This function is used in <a href="../demo/chart/area-world-population-distribution.php">the area chart demo</a> where * the chart is configured to render percentage values. * @example * var dataSource = new ludo.chart.DataSource({ data:[ { id: 1, label:'John', value:100 }, { id: 2, label:'Jane', value:245 }, { id: 3, label:'Martin', value:37 }, { id: 4, label:'Mary', value:99 }, { id: 5, label:'Johnny', value:127 }, { id: 6, label:'Catherine', value:55 }, { id: 7, label:'Tommy', value:18 } ], textOf:function(record, caller){ if(caller.type == 'chart.Tooltip'){ // Text for the tooltip return record.label + ': '+ record.value + ' (' + record.__percent + '%)'; } // Default text return record.label; }, valueOf:function(record, caller){ return record.value; } }); */ ludo.chart.DataSource = new Class({ Extends: ludo.dataSource.JSON, type: 'chart.DataSource', map: undefined, valueOf: undefined, textOf: undefined, valueKey: 'value', childKey: 'children', startAngle: 0, color: '#1976D2', colorOf: undefined, colorOverOf: undefined, strokeOf: undefined, strokeOverOf: undefined, colorUtilObj: undefined, count: undefined, highlighted: undefined, selectedRecord: undefined, /** * Max value in data array * @property {Number} maxVal * @memberof ludo.chart.DataSource.prototype */ maxVal: undefined, dataFor: undefined, sortFn: undefined, _maxIndexSum: undefined, /** * Aggregated max value in the data array. Sum value of child data. * @property {Number} maxValAggr * @memberof ludo.chart.DataSource.prototype * @example * [ * { * "country": "United Kingdom", * "children": [ * { "name":"0-14", "people" : 5000 }, { "name":"15-64", "people" : 20000 }, { "name":"65-", "people" : 4000 } * ] * }, * { * "country": "Germany", * "children": [ * { "name":"0-14", "people" : 6000 }, { "name":"15-64", "people" : 29000 }, { "name":"65-", "people" : 4000 } * ] * } * ] * // maxValAggr will here be 39000 (Sum children of "Germany"). */ maxValAggr: undefined, /** * Min value in data array * @property {Number} minVal * @memberof ludo.chart.DataSource.prototype */ minVal: undefined, minValAgr: undefined, _increments: undefined, increments: undefined, minBrightness: undefined, maxBrightness: undefined, minSaturation: undefined, maxSaturation: undefined, __construct: function (config) { this.setConfigParams(config, ['indexStartValueOf', 'minBrightness', 'maxBrightness', 'minSaturation', 'maxSaturation', 'shapeOf', 'dataFor', 'sortFn', 'shouldInheritColor', 'childKey', 'valueKey', 'color', 'valueOf', 'textOf', 'getText', 'max', 'min', 'increments', 'strokeOf', 'strokeOverOf', 'valueForDisplay']); this.parent(config); if (this.valueOf == undefined) { console.warn("Method valueOf(record, caller) not implemented in chart data source"); } if (this.textOf == undefined) { console.warn("Method textOf(record, caller) not implemented in chart data source"); } }, parseNewData: function (data) { this.handleData(data); this.parent(this.data); }, handleData: function (data) { if (!jQuery.isArray(data)) { data = [data]; } this.data = data; this.map = {}; this.startAngle = 0; this.minVal = undefined; this.minValAgr = undefined; this.maxVal = undefined; this.maxValAggr = undefined; this.count = this.getCount(this.data); this.parseChartBranch(this.data); this.setSumIndexes(this.data); if (this.sortFn != undefined) { this.sortBranch(this.data); } this.updateIncrements(); }, update: function (record) { this.handleData(this.data); this.fireEvent('update', [record, this]); }, updateIncrements: function () { if (this.increments == undefined)return; var inc = this.increments(this.minVal, this.maxVal, this); if (jQuery.isArray(inc)) { this._increments = inc; } else { this._increments = []; for (var i = this.min(), len = this.max(); i <= len; i += inc) { this._increments.push(i); } } }, getIncrements: function () { return this._increments; }, sum: function (branch) { var sum = 0; jQuery.each(branch, function (key, node) { sum += (this.value(node, this) || 0); }.bind(this)); return sum; }, getCount: function (data) { var count = 0; jQuery.each(data, function (key, node) { count++; if (node[this.childKey] != undefined && jQuery.isArray(node[this.childKey])) { count += this.getCount(node[this.childKey]); } }.bind(this)); return count; }, updateValues: function (branch, parent) { jQuery.each(branch, function (key, node) { var val = this.value(node, this); if (val == undefined || !isNaN(val)) { if (node[this.childKey] != undefined) { val = this.sum(node[this.childKey]); var vk = this.valueKey; if (vk != undefined) { node[vk] = val; } } } if (val != undefined && !isNaN(val)) { if (node[this.childKey] == undefined) { this.setMax(val); this.setMin(val); } this.setMinAgr(val); this.setMaxAgr(val); } }.bind(this)); }, setMax: function (val) { if (this.maxVal == undefined) { this.maxVal = val; } else { this.maxVal = Math.max(this.maxVal, val); } }, setMaxAgr: function (val) { if (this.maxValAggr == undefined) { this.maxValAggr = val; } else { this.maxValAggr = Math.max(this.maxValAggr, val); } }, setMin: function (val) { if (this.minVal == undefined) { this.minVal = val; } else { this.minVal = Math.min(this.minVal, val); } }, setMinAgr: function (val) { if (this.minValAgr == undefined) { this.minValAgr = val; } else { this.minValAgr = Math.min(this.minValAgr, val); } }, sortBranch: function (branch, parent) { if (parent != undefined && this.sortFn != undefined) { var fn = this.sortFn(parent, this); if (fn != undefined) { branch = branch.sort(fn); } } jQuery.each(branch, function (index, node) { node.__index = index; if (node[this.childKey] != undefined) { this.sortBranch(node[this.childKey], node); } }.bind(this)); }, parseChartBranch: function (branch, parent) { this.updateValues(branch, parent); var sum = this.sum(branch); var angle = parent && parent.angle ? parent.angle : 0; var i = 0; jQuery.each(branch, function (key, node) { branch[key] = this.parseNode(node, parent, i, sum, angle); if (node.__radians) { angle += node.__radians; } i++; if (node[this.childKey] != undefined) { this.parseChartBranch(node[this.childKey], node); } }.bind(this)); return branch; }, parseNode: function (node, parent, i, sum, angle) { node.__sum = sum; var val = this.value(node, this); if (!isNaN(val)) { node.__min = this.minVal; node.__max = this.maxVal; node.__maxAggr = this.maxValAggr; node.__minAgr = this.minValAgr; node.__fraction = val / sum; node.__percent = Math.round(node.__fraction * 100); node.__radians = ludo.geometry.toRadians(node.__fraction * 360); node.__angle = angle; } node.__count = this.count; if (node.index == undefined) { node.__index = i; } if (parent != undefined) { node.__parent = parent.id; node.getParent = function () { return parent; } } else { node.getParent = function () { return undefined; } } if (node.id == undefined) { node.id = 'chart-node-' + String.uniqueID(); } if (node.__uid == undefined) { node.__uid = 'chart-node-' + String.uniqueID(); } this.map[node.id] = node; var c = this.childKey; node.getChildren = function () { return node[c]; }; node.getChild = function (index) { return node[c][index]; }; this.setColor(node); return node; }, setSumIndexes: function (data) { var children = data[0].getChildren != undefined ? data[0].getChildren() : undefined; if (children == undefined)return; var sumIndexes = []; jQuery.each(data, function (index, record) { var c = record.getChildren(); if (c && c.length) { jQuery.each(c, function (i, childRecord) { childRecord.__val = this.valueOf(childRecord, this) || 0; if (sumIndexes.length == i) { sumIndexes.push(0); } childRecord.__indexStartVal = sumIndexes[i]; sumIndexes[i] += childRecord.__val; }.bind(this)); } }.bind(this)); this._maxIndexSum = undefined; jQuery.each(data, function (index, record) { var c = record.getChildren(); if (c && c.length) { jQuery.each(c, function (i, childRecord) { if (this._maxIndexSum == undefined) { this._maxIndexSum = sumIndexes[i]; } else { this._maxIndexSum = Math.max(this._maxIndexSum, sumIndexes[i]); } childRecord.__indexSum = sumIndexes[i]; childRecord.__indexFraction = childRecord.__val / sumIndexes[i]; childRecord.__indexStartFraction = childRecord.__indexStartVal / sumIndexes[i]; }.bind(this)); } }.bind(this)); }, maxIndexSum: function () { return this._maxIndexSum; }, val: function (id, value) { if (arguments.length == 2) { if (this.valueKey != undefined) { var rec = this.byId(id); if (rec) { rec[this.valueKey] = value; this.update(rec); } } } else { return this.valueOf(this.byId(id)); } }, byId: function (id) { if (this.map != undefined) { return this.map[id]; } return undefined; }, enterId: function (id) { this.enter(this.map[id]); }, enter: function (record) { this.highlighted = record; this.fireEvent('enter', [record, this]); this.fireEvent('enter' + record.__uid, [record, this]); }, leaveId: function (id) { this.leave(this.map[id]); }, leave: function (record) { this.fireEvent('leave', [record, this]); this.fireEvent('leave' + record.__uid, [record, this]); }, selectId: function (id) { this.select(this.map[id]); }, select: function (record) { if (this.selectedRecord != undefined) { this.fireEvent('blur', [this.selectedRecord, this]); this.fireEvent('blur' + this.selectedRecord.__uid, [this.selectedRecord, this]); } this.selectedRecord = record; this.fireEvent('select', [record, this]); this.fireEvent('select' + record.__uid, [record, this]); }, value: function (record, caller) { var val = this.valueOf != undefined ? this.valueOf(record, caller) : undefined; if (val == undefined && this.valueKey != undefined) { return record[this.valueKey]; } return val; }, isSelected: function (record) { return this.selectedRecord == record; }, hasData: function () { return this.data && this.data.length > 0; }, shapes: [ 'circle', 'rect', 'triangle', 'rotatedrect' ], shapeIndex: 0, sumBy: function (search, key) { return this._sumBy(this.data, search, key); }, _sumBy: function (array, search, key) { var sum = 0; jQuery.each(array, function (index, record) { if (this.isMatching(record, search)) { sum += record[key]; } var children = record.getChildren(); if (children && children.length > 0) { sum += this._sumBy(children, search, key, sum); } }.bind(this)); return sum; }, isMatching: function (record, search) { for (var key in search) { if (record[key] == undefined)return false; if (record[key] != search[key])return false; } return true; }, setColor: function (record) { var u = false; if (this.shouldInheritColor(record) && record.__parent) { var p = record.getParent(); record.__color = p.__color; record.__colorOver = p.__colorOver; record.__stroke = p.__stroke; record.__strokeOver = p.__strokeOver; record.__shape = p.__shape; this.color = this.colorUtil().offsetHue(this.color, (360 / (record.__count + 1))); return; } if (record.__shape == undefined) { record.__shape = this.shapes[this.shapeIndex++]; this.shapeIndex = this.shapeIndex % this.shapes.length; } if (record.__color == undefined) { if (this.colorOf != undefined) { record.__color = this.colorOf(record); } if (!record.__color) { record.__color = this.color; } u = true; } if (record.__colorOver == undefined) { record.__colorOver = this.colorUtil().brighten(record.__color, 7); if (record.__colorOver == record.__color) { record.__color = this.colorUtil().darken(record.__color, 7); } u = true; } if (record.__stroke == undefined && this.strokeOf != undefined) { record.__stroke = this.strokeOf(record, this); } if (record.__strokeOver == undefined && this.strokeOverOf != undefined) { record.__strokeOver = this.strokeOverOf(record, this); } this.adjustColor(record); if (u) { this.color = this.colorUtil().offsetHue(record.__color, (360 / (record.__count + 1))); } }, adjustColor: function (record) { var b, s; var u = this.colorUtil(); record.__color = this.rgba(record.__color); record.__colorOver = this.rgba(record.__colorOver); record.__stroke = this.rgba(record.__stroke); record.__strokeOver = this.rgba(record.__strokeOver); if (this.minBrightness != undefined) { b = u.brightness(record.__color); if (b < this.minBrightness) { record.__color = u.brightness(record.__color, this.minBrightness); } } if (this.maxBrightness != undefined) { b = u.brightness(record.__color); if (b > this.maxBrightness) { record.__color = u.brightness(record.__color, this.maxBrightness); } } if (this.minSaturation != undefined) { s = u.saturation(record.__color); if (s < this.minSaturation) { record.__color = u.saturation(record.__color, this.minSaturation); } } if (this.maxSaturation != undefined) { s = u.saturation(record.__color); if (s > this.maxSaturation) { record.__color = u.saturation(record.__color, this.maxSaturation); } } }, rgba:function(color){ if(color != undefined && color.indexOf('#') == 0 && color.length == 9){ var a = color.substr(7,2); var c = color.substr(0,7); var rgb = this.colorUtil().rgbColors(c); return 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + (a.toInt(16) / 255) + ')'; } return color; }, colorUtil: function () { if (this.colorUtilObj == undefined) { this.colorUtilObj = new ludo.color.Color(); } return this.colorUtilObj; }, getHighlighted: function () { return this.highlighted; }, getText: function (caller) { }, length: function () { return this.data.length; }, max: function () { return this.maxVal; }, min: function () { return Math.min(0, this.minVal); }, maxX:function(){ return this.max(); }, maxY:function(){ return this.max(); }, minX:function(){ return this.min(); }, minY:function(){ return this.min(); }, valueForDisplay: function (value) { return value; }, shouldInheritColor: function () { return false; }, getData: function (caller) { if (!caller) { return this.parent(); } else if (!caller.type) { console.warn("Type not set", caller); console.trace(); } caller = caller || this; var allData = this.parent(); var d = this.dataFor != undefined ? this.dataFor(caller, allData) : undefined; return d ? d : allData; }, shapeOf: function () { return undefined; }, indexStartValueOf: function (record) { return record.__indexStartVal || 0; } });