'use strict'; /*@ Ox.Chart Bar Chart options Options color <[n]|[[n]]|[128, 128, 128]> Bar color data {k: v, ...} or {k: {k: v, ...}, ...} formatKey Format function for keys keyAlign Alignment of keys keyWidth Width of keys limit Number of items, or 0 for all rows undocumented sort Sort title Chart title width Chart width self shared private variable ([options[, self]]) -> Chart object @*/ Ox.Chart = function(options, self) { self = self || {}; var that = Ox.Element({}, self) .defaults({ color: [128, 128, 128], data: {}, formatKey: null, keyAlign: 'right', keyWidth: 128, limit: 0, rows: 1, sort: {key: 'value', operator: '-'}, sortKey: null, title: '', width: 512 }) .options(options || {}) .update({ width: function() { self.$chart.empty(); renderChart(); } }) .addClass('OxChart'); self.valueWidth = self.options.width - self.options.keyWidth; self.keys = Object.keys(self.options.data); if (Ox.isObject(self.options.data[self.keys[0]])) { if (Ox.isUndefined(options.color)) { self.options.color = [ [192, 64, 64], [ 64, 192, 64], [ 64, 64, 192], [192, 192, 64], [ 64, 192, 192], [192, 64, 192], [192, 128, 64], [ 64, 192, 128], [128, 64, 192], [192, 64, 128], [128, 192, 64], [ 64, 128, 192] ]; } self.subData = {}; } // fixme: a lot of the sorting is unneeded, since it happens in the list self.sort = {}; self.totals = {}; Ox.forEach(self.options.data, function(value, key) { self.totals[key] = self.subData ? Ox.sum(value) : value; if (self.subData) { Object.keys(value).forEach(function(subKey) { self.subData[subKey] = (self.subData[subKey] || 0) + value[subKey]; }); } self.sort[key] = key.replace(/(\d+)/g, function(number) { return Ox.pad(parseInt(number, 10), 16); }); }); self.max = Ox.max(self.totals); self.sum = Ox.sum(self.totals); if (self.subData) { Ox.forEach(self.subData, function(subValue, subKey) { self.sort[subKey] = subKey.replace(/(\d+)/g, function(number) { return Ox.pad(parseInt(number, 10), 16); }); }); self.subKeys = Object.keys(self.subData).sort(function(a, b) { var aValue = self.subData[a], bValue = self.subData[b]; return a === '' ? 1 : b === '' ? -1 //: aValue < bValue ? 1 //: aValue > bValue ? -1 : self.sort[a] < self.sort[b] ? -1 : self.sort[a] > self.sort[b] ? 1 : 0; }); } self.items = self.keys.map(function(key) { return {key: key, value: self.options.data[key]}; }) .sort(function(a, b) { var key = self.options.sort.key, aValue = key == 'key' ? self.sort[a.key] : self.subData ? self.totals[a.key] : a.value, bValue = key == 'key' ? self.sort[b.key] : self.subData ? self.totals[b.key] : b.value; return aValue < bValue ? (self.options.sort.operator == '+' ? -1 : 1) : aValue > bValue ? (self.options.sort.operator == '+' ? 1 : -1) : key == 'value' && self.sort[a.key] < self.sort[b.key] ? -1 : key == 'value' && self.sort[a.key] > self.sort[b.key] ? 1 : 0; }); if (self.options.limit) { self.items = self.items.slice(0, self.options.limit); } if (self.options.rows == 2) { self.row = 0; } self.$title = Ox.Bar({size: 16}) .append( $('
') .css({margin: '1px 0 0 4px'}) .html(self.options.title) ) .appendTo(that); self.$chart = $('
') .css({position: 'absolute', top: '16px'}) .append(renderChart()) .appendTo(that); function getColumns() { return [ { align: self.options.keyAlign, format: self.options.formatKey, id: 'key', width: self.options.keyWidth, visible: true }, { format: renderValue, id: 'value', width: self.valueWidth, visible: true } ]; } function getWidths(values) { var max, maxKeys, total = Ox.sum(values), totalWidth = Math.ceil(total / self.max * self.valueWidth), widths = {}; Ox.forEach(values, function(value, key) { widths[key] = Math.round(value / total * totalWidth); }); while (Ox.sum(widths) != totalWidth) { max = Ox.max(widths); maxKeys = Object.keys(widths).filter(function(key) { return widths[key] == max; }); widths[maxKeys[0]] += Ox.sum(widths) < totalWidth ? 1 : -1; } return widths; } function renderChart() { that.css({ width: self.options.width + 'px', height: 16 + self.items.length * 16 + 'px', overflowY: 'hidden' }); return Ox.TableList({ columns: getColumns(), items: self.items, max: 0, min: 0, pageLength: self.items.length, sort: [self.options.sort], width: self.options.width, unique: 'key' }) .css({ left: 0, top: 0, width: self.options.width + 'px', height: self.items.length * 16 + 'px' }); } function renderValue(value, data) { var $bars = [], $element, colors = [], len, widths; if (!self.subData) { $element = $bars[0] = Ox.Element({ element: '
', tooltip: Ox.formatNumber(value) + ' (' + Ox.formatPercent(value * self.options.rows, self.sum, 2) + ')' }) .css({ width: Math.ceil(value / self.max * self.valueWidth) + 'px', height: '14px', borderRadius: '4px', marginLeft: '-4px' }); colors[0] = Ox.isFunction(self.options.color) ? self.options.color(data.key) : self.options.color; } else { $element = $('
') .css({ width: Math.round(self.totals[data.key] / self.max * self.valueWidth) + 'px', height: '14px', marginLeft: '-4px' }); len = Ox.len(value); widths = getWidths(value); self.subKeys.forEach(function(subKey, subKeyIndex) { var i = $bars.length, subValue = value[subKey]; if (subValue) { $bars[i] = Ox.Element({ element: '
', /* tooltip: Ox.formatNumber(self.totals[data.key]) + ' (' + Ox.formatPercent(self.totals[data.key] * self.options.rows, self.sum, 2) + ')' + '
' + subKey + ': ' + Ox.formatNumber(subValue) + ' (' + Ox.formatPercent(subValue, self.totals[data.key], 2) + ')' */ tooltip: Ox.formatNumber(self.totals[data.key]) + ' (' + Ox.formatPercent(self.totals[data.key] * self.options.rows, self.sum, 2) + ')' }) .css({ float: 'left', width: widths[subKey] + 'px', height: '14px', borderTopLeftRadius: i == 0 ? '4px' : 0, borderBottomLeftRadius: i == 0 ? '4px' : 0, borderTopRightRadius: i == len - 1 ? '4px' : 0, borderBottomRightRadius: i == len - 1 ? '4px' : 0 }) .appendTo($element); colors[i] = subKey == '' ? [128, 128, 128] : Ox.isArray(self.options.color) ? self.options.color[subKeyIndex % self.options.color.length] : Ox.isObject(self.options.color) ? self.options.color[subKey] : self.options.color(subKey); } }); } $bars.forEach(function($bar, i) { /* if (self.options.rows == 2) { colors[i] = colors[i].map(function(v) { return v + (self.row % 2 == 0 ? 16 : -16); }); } */ ['moz', 'o', 'webkit'].forEach(function(browser) { $bar.css({ backgroundImage: '-' + browser + '-linear-gradient(top, rgb(' + colors[i].map(function(v) { return Ox.limit(v + 16, 0, 255); }).join(', ') + '), rgb(' + colors[i].map(function(v) { return Ox.limit(v - 16, 0, 255); }) + '))' }); }); }); if (self.options.rows == 2) { self.row++; } return $element; } return that; };