/**
* Bluff - beautiful graphs in JavaScript
* ======================================
*
* Get the latest version and docs at http://bluff.jcoglan.com
* Based on Gruff by Geoffrey Grosenbach: http://github.com/topfunky/gruff
*
* Copyright (C) 2008-2010 James Coglan
*
* Released under the MIT license and the GPL v2.
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl-2.0.txt
**/
Bluff = {
// This is the version of Bluff you are using.
VERSION: '0.3.6',
array: function(list) {
if (list.length === undefined) return [list];
var ary = [], i = list.length;
while (i--) ary[i] = list[i];
return ary;
},
array_new: function(length, filler) {
var ary = [];
while (length--) ary.push(filler);
return ary;
},
each: function(list, block, context) {
for (var i = 0, n = list.length; i < n; i++) {
block.call(context || null, list[i], i);
}
},
index: function(list, needle) {
for (var i = 0, n = list.length; i < n; i++) {
if (list[i] === needle) return i;
}
return -1;
},
keys: function(object) {
var ary = [], key;
for (key in object) ary.push(key);
return ary;
},
map: function(list, block, context) {
var results = [];
this.each(list, function(item) {
results.push(block.call(context || null, item));
});
return results;
},
reverse_each: function(list, block, context) {
var i = list.length;
while (i--) block.call(context || null, list[i], i);
},
sum: function(list) {
var sum = 0, i = list.length;
while (i--) sum += list[i];
return sum;
},
Mini: {}
};
Bluff.Base = new JS.Class({
extend: {
// Draw extra lines showing where the margins and text centers are
DEBUG: false,
// Used for navigating the array of data to plot
DATA_LABEL_INDEX: 0,
DATA_VALUES_INDEX: 1,
DATA_COLOR_INDEX: 2,
// Space around text elements. Mostly used for vertical spacing
LEGEND_MARGIN: 20,
TITLE_MARGIN: 20,
LABEL_MARGIN: 10,
DEFAULT_MARGIN: 20,
DEFAULT_TARGET_WIDTH: 800,
THOUSAND_SEPARATOR: ','
},
// Blank space above the graph
top_margin: null,
// Blank space below the graph
bottom_margin: null,
// Blank space to the right of the graph
right_margin: null,
// Blank space to the left of the graph
left_margin: null,
// Blank space below the title
title_margin: null,
// Blank space below the legend
legend_margin: null,
// A hash of names for the individual columns, where the key is the array
// index for the column this label represents.
//
// Not all columns need to be named.
//
// Example: {0: 2005, 3: 2006, 5: 2007, 7: 2008}
labels: null,
// Used internally for spacing.
//
// By default, labels are centered over the point they represent.
center_labels_over_point: null,
// Used internally for horizontal graph types.
has_left_labels: null,
// A label for the bottom of the graph
x_axis_label: null,
// A label for the left side of the graph
y_axis_label: null,
// x_axis_increment: null,
// Manually set increment of the horizontal marking lines
y_axis_increment: null,
// Get or set the list of colors that will be used to draw the bars or lines.
colors: null,
// The large title of the graph displayed at the top
title: null,
// Font used for titles, labels, etc.
font: null,
font_color: null,
// Prevent drawing of line markers
hide_line_markers: null,
// Prevent drawing of the legend
hide_legend: null,
// Prevent drawing of the title
hide_title: null,
// Prevent drawing of line numbers
hide_line_numbers: null,
// Message shown when there is no data. Fits up to 20 characters. Defaults
// to "No Data."
no_data_message: null,
// The font size of the large title at the top of the graph
title_font_size: null,
// Optionally set the size of the font. Based on an 800x600px graph.
// Default is 20.
//
// Will be scaled down if graph is smaller than 800px wide.
legend_font_size: null,
// The font size of the labels around the graph
marker_font_size: null,
// The color of the auxiliary lines
marker_color: null,
// The number of horizontal lines shown for reference
marker_count: null,
// You can manually set a minimum value instead of having the values
// guessed for you.
//
// Set it after you have given all your data to the graph object.
minimum_value: null,
// You can manually set a maximum value, such as a percentage-based graph
// that always goes to 100.
//
// If you use this, you must set it after you have given all your data to
// the graph object.
maximum_value: null,
// Set to false if you don't want the data to be sorted with largest avg
// values at the back.
sort: null,
// Experimental
additional_line_values: null,
// Experimental
stacked: null,
// Optionally set the size of the colored box by each item in the legend.
// Default is 20.0
//
// Will be scaled down if graph is smaller than 800px wide.
legend_box_size: null,
// Set to true to enable tooltip displays
tooltips: false,
// If one numerical argument is given, the graph is drawn at 4/3 ratio
// according to the given width (800 results in 800x600, 400 gives 400x300,
// etc.).
//
// Or, send a geometry string for other ratios ('800x400', '400x225').
initialize: function(renderer, target_width) {
this._d = new Bluff.Renderer(renderer);
target_width = target_width || this.klass.DEFAULT_TARGET_WIDTH;
var geo;
if (typeof target_width !== 'number') {
geo = target_width.split('x');
this._columns = parseFloat(geo[0]);
this._rows = parseFloat(geo[1]);
} else {
this._columns = parseFloat(target_width);
this._rows = this._columns * 0.75;
}
this.initialize_ivars();
this._reset_themes();
this.theme_keynote();
this._listeners = {};
},
// Set instance variables for this object.
//
// Subclasses can override this, call super, then set values separately.
//
// This makes it possible to set defaults in a subclass but still allow
// developers to change this values in their program.
initialize_ivars: function() {
// Internal for calculations
this._raw_columns = 800;
this._raw_rows = 800 * (this._rows/this._columns);
this._column_count = 0;
this.marker_count = null;
this.maximum_value = this.minimum_value = null;
this._has_data = false;
this._data = [];
this.labels = {};
this._labels_seen = {};
this.sort = true;
this.title = null;
this._scale = this._columns / this._raw_columns;
this.marker_font_size = 21.0;
this.legend_font_size = 20.0;
this.title_font_size = 36.0;
this.top_margin = this.bottom_margin =
this.left_margin = this.right_margin = this.klass.DEFAULT_MARGIN;
this.legend_margin = this.klass.LEGEND_MARGIN;
this.title_margin = this.klass.TITLE_MARGIN;
this.legend_box_size = 20.0;
this.no_data_message = "No Data";
this.hide_line_markers = this.hide_legend = this.hide_title = this.hide_line_numbers = false;
this.center_labels_over_point = true;
this.has_left_labels = false;
this.additional_line_values = [];
this._additional_line_colors = [];
this._theme_options = {};
this.x_axis_label = this.y_axis_label = null;
this.y_axis_increment = null;
this.stacked = null;
this._norm_data = null;
},
// Sets the top, bottom, left and right margins to +margin+.
set_margins: function(margin) {
this.top_margin = this.left_margin = this.right_margin = this.bottom_margin = margin;
},
// Sets the font for graph text to the font at +font_path+.
set_font: function(font_path) {
this.font = font_path;
this._d.font = this.font;
},
// Add a color to the list of available colors for lines.
//
// Example:
// add_color('#c0e9d3')
add_color: function(colorname) {
this.colors.push(colorname);
},
// Replace the entire color list with a new array of colors. Also
// aliased as the colors= setter method.
//
// If you specify fewer colors than the number of datasets you intend
// to draw, 'increment_color' will cycle through the array, reusing
// colors as needed.
//
// Note that (as with the 'set_theme' method), you should set up the color
// list before you send your data (via the 'data' method). Calls to the
// 'data' method made prior to this call will use whatever color scheme
// was in place at the time data was called.
//
// Example:
// replace_colors ['#cc99cc', '#d9e043', '#34d8a2']
replace_colors: function(color_list) {
this.colors = color_list || [];
this._color_index = 0;
},
// You can set a theme manually. Assign a hash to this method before you
// send your data.
//
// graph.set_theme({
// colors: ['orange', 'purple', 'green', 'white', 'red'],
// marker_color: 'blue',
// background_colors: ['black', 'grey']
// })
//
// background_image: 'squirrel.png' is also possible.
//
// (Or hopefully something better looking than that.)
//
set_theme: function(options) {
this._reset_themes();
this._theme_options = {
colors: ['black', 'white'],
additional_line_colors: [],
marker_color: 'white',
font_color: 'black',
background_colors: null,
background_image: null
};
for (var key in options) this._theme_options[key] = options[key];
this.colors = this._theme_options.colors;
this.marker_color = this._theme_options.marker_color;
this.font_color = this._theme_options.font_color || this.marker_color;
this._additional_line_colors = this._theme_options.additional_line_colors;
this._render_background();
},
// Set just the background colors
set_background: function(options) {
if (options.colors)
this._theme_options.background_colors = options.colors;
if (options.image)
this._theme_options.background_image = options.image;
this._render_background();
},
// A color scheme similar to the popular presentation software.
theme_keynote: function() {
// Colors
this._blue = '#6886B4';
this._yellow = '#FDD84E';
this._green = '#72AE6E';
this._red = '#D1695E';
this._purple = '#8A6EAF';
this._orange = '#EFAA43';
this._white = 'white';
this.colors = [this._yellow, this._blue, this._green, this._red, this._purple, this._orange, this._white];
this.set_theme({
colors: this.colors,
marker_color: 'white',
font_color: 'white',
background_colors: ['black', '#4a465a']
});
},
// A color scheme plucked from the colors on the popular usability blog.
theme_37signals: function() {
// Colors
this._green = '#339933';
this._purple = '#cc99cc';
this._blue = '#336699';
this._yellow = '#FFF804';
this._red = '#ff0000';
this._orange = '#cf5910';
this._black = 'black';
this.colors = [this._yellow, this._blue, this._green, this._red, this._purple, this._orange, this._black];
this.set_theme({
colors: this.colors,
marker_color: 'black',
font_color: 'black',
background_colors: ['#d1edf5', 'white']
});
},
// A color scheme from the colors used on the 2005 Rails keynote
// presentation at RubyConf.
theme_rails_keynote: function() {
// Colors
this._green = '#00ff00';
this._grey = '#333333';
this._orange = '#ff5d00';
this._red = '#f61100';
this._white = 'white';
this._light_grey = '#999999';
this._black = 'black';
this.colors = [this._green, this._grey, this._orange, this._red, this._white, this._light_grey, this._black];
this.set_theme({
colors: this.colors,
marker_color: 'white',
font_color: 'white',
background_colors: ['#0083a3', '#0083a3']
});
},
// A color scheme similar to that used on the popular podcast site.
theme_odeo: function() {
// Colors
this._grey = '#202020';
this._white = 'white';
this._dark_pink = '#a21764';
this._green = '#8ab438';
this._light_grey = '#999999';
this._dark_blue = '#3a5b87';
this._black = 'black';
this.colors = [this._grey, this._white, this._dark_blue, this._dark_pink, this._green, this._light_grey, this._black];
this.set_theme({
colors: this.colors,
marker_color: 'white',
font_color: 'white',
background_colors: ['#ff47a4', '#ff1f81']
});
},
// A pastel theme
theme_pastel: function() {
// Colors
this.colors = [
'#a9dada', // blue
'#aedaa9', // green
'#daaea9', // peach
'#dadaa9', // yellow
'#a9a9da', // dk purple
'#daaeda', // purple
'#dadada' // grey
];
this.set_theme({
colors: this.colors,
marker_color: '#aea9a9', // Grey
font_color: 'black',
background_colors: 'white'
});
},
// A greyscale theme
theme_greyscale: function() {
// Colors
this.colors = [
'#282828', //
'#383838', //
'#686868', //
'#989898', //
'#c8c8c8', //
'#e8e8e8' //
];
this.set_theme({
colors: this.colors,
marker_color: '#aea9a9', // Grey
font_color: 'black',
background_colors: 'white'
});
},
// Parameters are an array where the first element is the name of the dataset
// and the value is an array of values to plot.
//
// Can be called multiple times with different datasets for a multi-valued
// graph.
//
// If the color argument is nil, the next color from the default theme will
// be used.
//
// NOTE: If you want to use a preset theme, you must set it before calling
// data().
//
// Example:
// data("Bart S.", [95, 45, 78, 89, 88, 76], '#ffcc00')
data: function(name, data_points, color) {
data_points = (data_points === undefined) ? [] : data_points;
color = color || null;
data_points = Bluff.array(data_points); // make sure it's an array
this._data.push([name, data_points, (color || this._increment_color())]);
// Set column count if this is larger than previous counts
this._column_count = (data_points.length > this._column_count) ? data_points.length : this._column_count;
// Pre-normalize
Bluff.each(data_points, function(data_point, index) {
if (data_point === undefined) return;
// Setup max/min so spread starts at the low end of the data points
if (this.maximum_value === null && this.minimum_value === null)
this.maximum_value = this.minimum_value = data_point;
// TODO Doesn't work with stacked bar graphs
// Original: @maximum_value = _larger_than_max?(data_point, index) ? max(data_point, index) : @maximum_value
this.maximum_value = this._larger_than_max(data_point) ? data_point : this.maximum_value;
if (this.maximum_value >= 0) this._has_data = true;
this.minimum_value = this._less_than_min(data_point) ? data_point : this.minimum_value;
if (this.minimum_value < 0) this._has_data = true;
}, this);
},
// Overridden by subclasses to do the actual plotting of the graph.
//
// Subclasses should start by calling super() for this method.
draw: function() {
if (this.stacked) this._make_stacked();
this._setup_drawing();
this._debug(function() {
// Outer margin
this._d.rectangle(this.left_margin, this.top_margin,
this._raw_columns - this.right_margin, this._raw_rows - this.bottom_margin);
// Graph area box
this._d.rectangle(this._graph_left, this._graph_top, this._graph_right, this._graph_bottom);
});
},
clear: function() {
this._render_background();
},
on: function(eventType, callback, context) {
var list = this._listeners[eventType] = this._listeners[eventType] || [];
list.push([callback, context]);
},
trigger: function(eventType, data) {
var list = this._listeners[eventType];
if (!list) return;
Bluff.each(list, function(listener) {
listener[0].call(listener[1], data);
});
},
// Calculates size of drawable area and draws the decorations.
//
// * line markers
// * legend
// * title
_setup_drawing: function() {
// Maybe should be done in one of the following functions for more granularity.
if (!this._has_data) return this._draw_no_data();
this._normalize();
this._setup_graph_measurements();
if (this.sort) this._sort_norm_data();
this._draw_legend();
this._draw_line_markers();
this._draw_axis_labels();
this._draw_title();
},
// Make copy of data with values scaled between 0-100
_normalize: function(force) {
if (this._norm_data === null || force === true) {
this._norm_data = [];
if (!this._has_data) return;
this._calculate_spread();
Bluff.each(this._data, function(data_row) {
var norm_data_points = [];
Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point) {
if (data_point === null || data_point === undefined)
norm_data_points.push(null);
else
norm_data_points.push((data_point - this.minimum_value) / this._spread);
}, this);
this._norm_data.push([data_row[this.klass.DATA_LABEL_INDEX], norm_data_points, data_row[this.klass.DATA_COLOR_INDEX]]);
}, this);
}
},
_calculate_spread: function() {
this._spread = this.maximum_value - this.minimum_value;
this._spread = this._spread > 0 ? this._spread : 1;
var power = Math.round(Math.LOG10E*Math.log(this._spread));
this._significant_digits = Math.pow(10, 3 - power);
},
// Calculates size of drawable area, general font dimensions, etc.
_setup_graph_measurements: function() {
this._marker_caps_height = this.hide_line_markers ? 0 :
this._calculate_caps_height(this.marker_font_size);
this._title_caps_height = this.hide_title ? 0 :
this._calculate_caps_height(this.title_font_size);
this._legend_caps_height = this.hide_legend ? 0 :
this._calculate_caps_height(this.legend_font_size);
var longest_label,
longest_left_label_width,
line_number_width,
last_label,
extra_room_for_long_label,
x_axis_label_height,
key;
if (this.hide_line_markers) {
this._graph_left = this.left_margin;
this._graph_right_margin = this.right_margin;
this._graph_bottom_margin = this.bottom_margin;
} else {
longest_left_label_width = 0;
if (this.has_left_labels) {
longest_label = '';
for (key in this.labels) {
longest_label = longest_label.length > this.labels[key].length
? longest_label
: this.labels[key];
}
longest_left_label_width = this._calculate_width(this.marker_font_size, longest_label) * 1.25;
} else {
longest_left_label_width = this._calculate_width(this.marker_font_size, this._label(this.maximum_value));
}
// Shift graph if left line numbers are hidden
line_number_width = this.hide_line_numbers && !this.has_left_labels ?
0.0 :
longest_left_label_width + this.klass.LABEL_MARGIN * 2;
this._graph_left = this.left_margin +
line_number_width +
(this.y_axis_label === null ? 0.0 : this._marker_caps_height + this.klass.LABEL_MARGIN * 2);
// Make space for half the width of the rightmost column label.
// Might be greater than the number of columns if between-style bar markers are used.
last_label = -Infinity;
for (key in this.labels)
last_label = last_label > Number(key) ? last_label : Number(key);
last_label = Math.round(last_label);
extra_room_for_long_label = (last_label >= (this._column_count-1) && this.center_labels_over_point) ?
this._calculate_width(this.marker_font_size, this.labels[last_label]) / 2 :
0;
this._graph_right_margin = this.right_margin + extra_room_for_long_label;
this._graph_bottom_margin = this.bottom_margin +
this._marker_caps_height + this.klass.LABEL_MARGIN;
}
this._graph_right = this._raw_columns - this._graph_right_margin;
this._graph_width = this._raw_columns - this._graph_left - this._graph_right_margin;
// When hide_title, leave a title_margin space for aesthetics.
// Same with hide_legend
this._graph_top = this.top_margin +
(this.hide_title ? this.title_margin : this._title_caps_height + this.title_margin ) +
(this.hide_legend ? this.legend_margin : this._legend_caps_height + this.legend_margin);
x_axis_label_height = (this.x_axis_label === null) ? 0.0 :
this._marker_caps_height + this.klass.LABEL_MARGIN;
this._graph_bottom = this._raw_rows - this._graph_bottom_margin - x_axis_label_height;
this._graph_height = this._graph_bottom - this._graph_top;
},
// Draw the optional labels for the x axis and y axis.
_draw_axis_labels: function() {
if (this.x_axis_label) {
// X Axis
// Centered vertically and horizontally by setting the
// height to 1.0 and the width to the width of the graph.
var x_axis_label_y_coordinate = this._graph_bottom + this.klass.LABEL_MARGIN * 2 + this._marker_caps_height;
// TODO Center between graph area
this._d.fill = this.font_color;
if (this.font) this._d.font = this.font;
this._d.stroke = 'transparent';
this._d.pointsize = this._scale_fontsize(this.marker_font_size);
this._d.gravity = 'north';
this._d.annotate_scaled(
this._raw_columns, 1.0,
0.0, x_axis_label_y_coordinate,
this.x_axis_label, this._scale);
this._debug(function() {
this._d.line(0.0, x_axis_label_y_coordinate, this._raw_columns, x_axis_label_y_coordinate);
});
}
// TODO Y label (not generally possible in browsers)
},
// Draws horizontal background lines and labels
_draw_line_markers: function() {
if (this.hide_line_markers) return;
if (this.y_axis_increment === null) {
// Try to use a number of horizontal lines that will come out even.
//
// TODO Do the same for larger numbers...100, 75, 50, 25
if (this.marker_count === null) {
Bluff.each([3,4,5,6,7], function(lines) {
if (!this.marker_count && this._spread % lines === 0)
this.marker_count = lines;
}, this);
this.marker_count = this.marker_count || 4;
}
this._increment = (this._spread > 0) ? this._significant(this._spread / this.marker_count) : 1;
} else {
// TODO Make this work for negative values
this.maximum_value = Math.max(Math.ceil(this.maximum_value), this.y_axis_increment);
this.minimum_value = Math.floor(this.minimum_value);
this._calculate_spread();
this._normalize(true);
this.marker_count = Math.round(this._spread / this.y_axis_increment);
this._increment = this.y_axis_increment;
}
this._increment_scaled = this._graph_height / (this._spread / this._increment);
// Draw horizontal line markers and annotate with numbers
var index, n, y, marker_label;
for (index = 0, n = this.marker_count; index <= n; index++) {
y = this._graph_top + this._graph_height - index * this._increment_scaled;
this._d.stroke = this.marker_color;
this._d.stroke_width = 1;
this._d.line(this._graph_left, y, this._graph_right, y);
marker_label = index * this._increment + this.minimum_value;
if (!this.hide_line_numbers) {
this._d.fill = this.font_color;
if (this.font) this._d.font = this.font;
this._d.font_weight = 'normal';
this._d.stroke = 'transparent';
this._d.pointsize = this._scale_fontsize(this.marker_font_size);
this._d.gravity = 'east';
// Vertically center with 1.0 for the height
this._d.annotate_scaled(this._graph_left - this.klass.LABEL_MARGIN,
1.0, 0.0, y,
this._label(marker_label), this._scale);
}
}
},
_center: function(size) {
return (this._raw_columns - size) / 2;
},
// Draws a legend with the names of the datasets matched to the colors used
// to draw them.
_draw_legend: function() {
if (this.hide_legend) return;
this._legend_labels = Bluff.map(this._data, function(item) {
return item[this.klass.DATA_LABEL_INDEX];
}, this);
var legend_square_width = this.legend_box_size; // small square with color of this item
// May fix legend drawing problem at small sizes
if (this.font) this._d.font = this.font;
this._d.pointsize = this.legend_font_size;
var label_widths = [[]]; // Used to calculate line wrap
Bluff.each(this._legend_labels, function(label) {
var last = label_widths.length - 1;
var metrics = this._d.get_type_metrics(label);
var label_width = metrics.width + legend_square_width * 2.7;
label_widths[last].push(label_width);
if (Bluff.sum(label_widths[last]) > (this._raw_columns * 0.9))
label_widths.push([label_widths[last].pop()]);
}, this);
var current_x_offset = this._center(Bluff.sum(label_widths[0]));
var current_y_offset = this.hide_title ?
this.top_margin + this.title_margin :
this.top_margin + this.title_margin + this._title_caps_height;
this._debug(function() {
this._d.stroke_width = 1;
this._d.line(0, current_y_offset, this._raw_columns, current_y_offset);
});
Bluff.each(this._legend_labels, function(legend_label, index) {
// Draw label
this._d.fill = this.font_color;
if (this.font) this._d.font = this.font;
this._d.pointsize = this._scale_fontsize(this.legend_font_size);
this._d.stroke = 'transparent';
this._d.font_weight = 'normal';
this._d.gravity = 'west';
this._d.annotate_scaled(this._raw_columns, 1.0,
current_x_offset + (legend_square_width * 1.7), current_y_offset,
legend_label, this._scale);
// Now draw box with color of this dataset
this._d.stroke = 'transparent';
this._d.fill = this._data[index][this.klass.DATA_COLOR_INDEX];
this._d.rectangle(current_x_offset,
current_y_offset - legend_square_width / 2.0,
current_x_offset + legend_square_width,
current_y_offset + legend_square_width / 2.0);
this._d.pointsize = this.legend_font_size;
var metrics = this._d.get_type_metrics(legend_label);
var current_string_offset = metrics.width + (legend_square_width * 2.7),
line_height;
// Handle wrapping
label_widths[0].shift();
if (label_widths[0].length == 0) {
this._debug(function() {
this._d.line(0.0, current_y_offset, this._raw_columns, current_y_offset);
});
label_widths.shift();
if (label_widths.length > 0) current_x_offset = this._center(Bluff.sum(label_widths[0]));
line_height = Math.max(this._legend_caps_height, legend_square_width) + this.legend_margin;
if (label_widths.length > 0) {
// Wrap to next line and shrink available graph dimensions
current_y_offset += line_height;
this._graph_top += line_height;
this._graph_height = this._graph_bottom - this._graph_top;
}
} else {
current_x_offset += current_string_offset;
}
}, this);
this._color_index = 0;
},
// Draws a title on the graph.
_draw_title: function() {
if (this.hide_title || !this.title) return;
this._d.fill = this.font_color;
if (this.font) this._d.font = this.font;
this._d.pointsize = this._scale_fontsize(this.title_font_size);
this._d.font_weight = 'bold';
this._d.gravity = 'north';
this._d.annotate_scaled(this._raw_columns, 1.0,
0, this.top_margin,
this.title, this._scale);
},
// Draws column labels below graph, centered over x_offset
//--
// TODO Allow WestGravity as an option
_draw_label: function(x_offset, index) {
if (this.hide_line_markers) return;
var y_offset;
if (this.labels[index] && !this._labels_seen[index]) {
y_offset = this._graph_bottom + this.klass.LABEL_MARGIN;
this._d.fill = this.font_color;
if (this.font) this._d.font = this.font;
this._d.stroke = 'transparent';
this._d.font_weight = 'normal';
this._d.pointsize = this._scale_fontsize(this.marker_font_size);
this._d.gravity = 'north';
this._d.annotate_scaled(1.0, 1.0,
x_offset, y_offset,
this.labels[index], this._scale);
this._labels_seen[index] = true;
this._debug(function() {
this._d.stroke_width = 1;
this._d.line(0.0, y_offset, this._raw_columns, y_offset);
});
}
},
// Creates a mouse hover target rectangle for tooltip displays
_draw_tooltip: function(left, top, width, height, name, color, data, index) {
if (!this.tooltips) return;
var node = this._d.tooltip(left, top, width, height, name, color, data);
Bluff.Event.observe(node, 'click', function() {
var point = {
series: name,
label: this.labels[index],
value: data,
color: color
};
this.trigger('click:datapoint', point);
}, this);
},
// Shows an error message because you have no data.
_draw_no_data: function() {
this._d.fill = this.font_color;
if (this.font) this._d.font = this.font;
this._d.stroke = 'transparent';
this._d.font_weight = 'normal';
this._d.pointsize = this._scale_fontsize(80);
this._d.gravity = 'center';
this._d.annotate_scaled(this._raw_columns, this._raw_rows/2,
0, 10,
this.no_data_message, this._scale);
},
// Finds the best background to render based on the provided theme options.
_render_background: function() {
var colors = this._theme_options.background_colors;
switch (true) {
case colors instanceof Array:
this._render_gradiated_background.apply(this, colors);
break;
case typeof colors === 'string':
this._render_solid_background(colors);
break;
default:
this._render_image_background(this._theme_options.background_image);
break;
}
},
// Make a new image at the current size with a solid +color+.
_render_solid_background: function(color) {
this._d.render_solid_background(this._columns, this._rows, color);
},
// Use with a theme definition method to draw a gradiated background.
_render_gradiated_background: function(top_color, bottom_color) {
this._d.render_gradiated_background(this._columns, this._rows, top_color, bottom_color);
},
// Use with a theme to use an image (800x600 original) background.
_render_image_background: function(image_path) {
// TODO
},
// Resets everything to defaults (except data).
_reset_themes: function() {
this._color_index = 0;
this._labels_seen = {};
this._theme_options = {};
this._d.scale(this._scale, this._scale);
},
_scale_value: function(value) {
return this._scale * value;
},
// Return a comparable fontsize for the current graph.
_scale_fontsize: function(value) {
var new_fontsize = value * this._scale;
return new_fontsize;
},
_clip_value_if_greater_than: function(value, max_value) {
return (value > max_value) ? max_value : value;
},
// Overridden by subclasses such as stacked bar.
_larger_than_max: function(data_point, index) {
return data_point > this.maximum_value;
},
_less_than_min: function(data_point, index) {
return data_point < this.minimum_value;
},
// Overridden by subclasses that need it.
_max: function(data_point, index) {
return data_point;
},
// Overridden by subclasses that need it.
_min: function(data_point, index) {
return data_point;
},
_significant: function(inc) {
if (inc == 0) return 1.0;
var factor = 1.0;
while (inc < 10) {
inc *= 10;
factor /= 10;
}
while (inc > 100) {
inc /= 10;
factor *= 10;
}
return Math.floor(inc) * factor;
},
// Sort with largest overall summed value at front of array so it shows up
// correctly in the drawn graph.
_sort_norm_data: function() {
var sums = this._sums, index = this.klass.DATA_VALUES_INDEX;
this._norm_data.sort(function(a,b) {
return sums(b[index]) - sums(a[index]);
});
this._data.sort(function(a,b) {
return sums(b[index]) - sums(a[index]);
});
},
_sums: function(data_set) {
var total_sum = 0;
Bluff.each(data_set, function(num) { total_sum += (num || 0) });
return total_sum;
},
_make_stacked: function() {
var stacked_values = [], i = this._column_count;
while (i--) stacked_values[i] = 0;
Bluff.each(this._data, function(value_set) {
Bluff.each(value_set[this.klass.DATA_VALUES_INDEX], function(value, index) {
stacked_values[index] += value;
}, this);
value_set[this.klass.DATA_VALUES_INDEX] = Bluff.array(stacked_values);
}, this);
},
// Takes a block and draws it if DEBUG is true.
//
// Example:
// debug { @d.rectangle x1, y1, x2, y2 }
_debug: function(block) {
if (this.klass.DEBUG) {
this._d.fill = 'transparent';
this._d.stroke = 'turquoise';
block.call(this);
}
},
// Returns the next color in your color list.
_increment_color: function() {
var offset = this._color_index;
this._color_index = (this._color_index + 1) % this.colors.length;
return this.colors[offset];
},
// Return a formatted string representing a number value that should be
// printed as a label.
_label: function(value) {
var sep = this.klass.THOUSAND_SEPARATOR,
label = (this._spread % this.marker_count == 0 || this.y_axis_increment !== null)
? String(Math.round(value))
: String(Math.floor(value * this._significant_digits)/this._significant_digits);
var parts = label.split('.');
parts[0] = parts[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1' + sep);
return parts.join('.');
},
// Returns the height of the capital letter 'X' for the current font and
// size.
//
// Not scaled since it deals with dimensions that the regular scaling will
// handle.
_calculate_caps_height: function(font_size) {
return this._d.caps_height(font_size);
},
// Returns the width of a string at this pointsize.
//
// Not scaled since it deals with dimensions that the regular
// scaling will handle.
_calculate_width: function(font_size, text) {
return this._d.text_width(font_size, text);
}
});
Bluff.Area = new JS.Class(Bluff.Base, {
draw: function() {
this.callSuper();
if (!this._has_data) return;
this._x_increment = this._graph_width / (this._column_count - 1);
this._d.stroke = 'transparent';
Bluff.each(this._norm_data, function(data_row) {
var poly_points = [],
prev_x = 0.0,
prev_y = 0.0;
Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, index) {
// Use incremented x and scaled y
var new_x = this._graph_left + (this._x_increment * index);
var new_y = this._graph_top + (this._graph_height - data_point * this._graph_height);
if (prev_x > 0 && prev_y > 0) {
poly_points.push(new_x);
poly_points.push(new_y);
// this._d.polyline(prev_x, prev_y, new_x, new_y);
} else {
poly_points.push(this._graph_left);
poly_points.push(this._graph_bottom - 1);
poly_points.push(new_x);
poly_points.push(new_y);
// this._d.polyline(this._graph_left, this._graph_bottom, new_x, new_y);
}
this._draw_label(new_x, index);
prev_x = new_x;
prev_y = new_y;
}, this);
// Add closing points, draw polygon
poly_points.push(this._graph_right);
poly_points.push(this._graph_bottom - 1);
poly_points.push(this._graph_left);
poly_points.push(this._graph_bottom - 1);
this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
this._d.polyline(poly_points);
}, this);
}
});
// This class perfoms the y coordinats conversion for the bar class.
//
// There are three cases:
//
// 1. Bars all go from zero in positive direction
// 2. Bars all go from zero to negative direction
// 3. Bars either go from zero to positive or from zero to negative
//
Bluff.BarConversion = new JS.Class({
mode: null,
zero: null,
graph_top: null,
graph_height: null,
minimum_value: null,
spread: null,
getLeftYRightYscaled: function(data_point, result) {
var val;
switch (this.mode) {
case 1: // Case one
// minimum value >= 0 ( only positiv values )
result[0] = this.graph_top + this.graph_height*(1 - data_point) + 1;
result[1] = this.graph_top + this.graph_height - 1;
break;
case 2: // Case two
// only negativ values
result[0] = this.graph_top + 1;
result[1] = this.graph_top + this.graph_height*(1 - data_point) - 1;
break;
case 3: // Case three
// positiv and negativ values
val = data_point-this.minimum_value/this.spread;
if ( data_point >= this.zero ) {
result[0] = this.graph_top + this.graph_height*(1 - (val-this.zero)) + 1;
result[1] = this.graph_top + this.graph_height*(1 - this.zero) - 1;
} else {
result[0] = this.graph_top + this.graph_height*(1 - (val-this.zero)) + 1;
result[1] = this.graph_top + this.graph_height*(1 - this.zero) - 1;
}
break;
default:
result[0] = 0.0;
result[1] = 0.0;
}
}
});
Bluff.Bar = new JS.Class(Bluff.Base, {
// Spacing factor applied between bars
bar_spacing: 0.9,
draw: function() {
// Labels will be centered over the left of the bar if
// there are more labels than columns. This is basically the same
// as where it would be for a line graph.
this.center_labels_over_point = (Bluff.keys(this.labels).length > this._column_count);
this.callSuper();
if (!this._has_data) return;
this._draw_bars();
},
_draw_bars: function() {
this._bar_width = this._graph_width / (this._column_count * this._data.length);
var padding = (this._bar_width * (1 - this.bar_spacing)) / 2;
this._d.stroke_opacity = 0.0;
// Setup the BarConversion Object
var conversion = new Bluff.BarConversion();
conversion.graph_height = this._graph_height;
conversion.graph_top = this._graph_top;
// Set up the right mode [1,2,3] see BarConversion for further explanation
if (this.minimum_value >= 0) {
// all bars go from zero to positiv
conversion.mode = 1;
} else {
// all bars go from 0 to negativ
if (this.maximum_value <= 0) {
conversion.mode = 2;
} else {
// bars either go from zero to negativ or to positiv
conversion.mode = 3;
conversion.spread = this._spread;
conversion.minimum_value = this.minimum_value;
conversion.zero = -this.minimum_value/this._spread;
}
}
// iterate over all normalised data
Bluff.each(this._norm_data, function(data_row, row_index) {
var raw_data = this._data[row_index][this.klass.DATA_VALUES_INDEX];
Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, point_index) {
// Use incremented x and scaled y
// x
var left_x = this._graph_left + (this._bar_width * (row_index + point_index + ((this._data.length - 1) * point_index))) + padding;
var right_x = left_x + this._bar_width * this.bar_spacing;
// y
var conv = [];
conversion.getLeftYRightYscaled(data_point, conv);
// create new bar
this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
this._d.rectangle(left_x, conv[0], right_x, conv[1]);
// create tooltip target
this._draw_tooltip(left_x, conv[0],
right_x - left_x, conv[1] - conv[0],
data_row[this.klass.DATA_LABEL_INDEX],
data_row[this.klass.DATA_COLOR_INDEX],
raw_data[point_index], point_index);
// Calculate center based on bar_width and current row
var label_center = this._graph_left +
(this._data.length * this._bar_width * point_index) +
(this._data.length * this._bar_width / 2.0);
// Subtract half a bar width to center left if requested
this._draw_label(label_center - (this.center_labels_over_point ? this._bar_width / 2.0 : 0.0), point_index);
}, this);
}, this);
// Draw the last label if requested
if (this.center_labels_over_point) this._draw_label(this._graph_right, this._column_count);
}
});
// Here's how to make a Line graph:
//
// g = new Bluff.Line('canvasId');
// g.title = "A Line Graph";
// g.data('Fries', [20, 23, 19, 8]);
// g.data('Hamburgers', [50, 19, 99, 29]);
// g.draw();
//
// There are also other options described below, such as #baseline_value, #baseline_color, #hide_dots, and #hide_lines.
Bluff.Line = new JS.Class(Bluff.Base, {
// Draw a dashed line at the given value
baseline_value: null,
// Color of the baseline
baseline_color: null,
// Dimensions of lines and dots; calculated based on dataset size if left unspecified
line_width: null,
dot_radius: null,
// Hide parts of the graph to fit more datapoints, or for a different appearance.
hide_dots: null,
hide_lines: null,
// Call with target pixel width of graph (800, 400, 300), and/or 'false' to omit lines (points only).
//
// g = new Bluff.Line('canvasId', 400) // 400px wide with lines
//
// g = new Bluff.Line('canvasId', 400, false) // 400px wide, no lines (for backwards compatibility)
//
// g = new Bluff.Line('canvasId', false) // Defaults to 800px wide, no lines (for backwards compatibility)
//
// The preferred way is to call hide_dots or hide_lines instead.
initialize: function(renderer) {
if (arguments.length > 3) throw 'Wrong number of arguments';
if (arguments.length === 1 || (typeof arguments[1] !== 'number' && typeof arguments[1] !== 'string'))
this.callSuper(renderer, null);
else
this.callSuper();
this.hide_dots = this.hide_lines = false;
this.baseline_color = 'red';
this.baseline_value = null;
},
draw: function() {
this.callSuper();
if (!this._has_data) return;
// Check to see if more than one datapoint was given. NaN can result otherwise.
this.x_increment = (this._column_count > 1) ? (this._graph_width / (this._column_count - 1)) : this._graph_width;
var level;
if (this._norm_baseline !== undefined) {
level = this._graph_top + (this._graph_height - this._norm_baseline * this._graph_height);
this._d.push();
this._d.stroke = this.baseline_color;
this._d.fill_opacity = 0.0;
// this._d.stroke_dasharray(10, 20);
this._d.stroke_width = 3.0;
this._d.line(this._graph_left, level, this._graph_left + this._graph_width, level);
this._d.pop();
}
Bluff.each(this._norm_data, function(data_row, row_index) {
var prev_x = null, prev_y = null;
var raw_data = this._data[row_index][this.klass.DATA_VALUES_INDEX];
this._one_point = this._contains_one_point_only(data_row);
Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, index) {
var new_x = this._graph_left + (this.x_increment * index);
if (typeof data_point !== 'number') return;
this._draw_label(new_x, index);
var new_y = this._graph_top + (this._graph_height - data_point * this._graph_height);
// Reset each time to avoid thin-line errors
this._d.stroke = data_row[this.klass.DATA_COLOR_INDEX];
this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
this._d.stroke_opacity = 1.0;
this._d.stroke_width = this.line_width ||
this._clip_value_if_greater_than(this._columns / (this._norm_data[0][this.klass.DATA_VALUES_INDEX].length * 6), 3.0);
var circle_radius = this.dot_radius ||
this._clip_value_if_greater_than(this._columns / (this._norm_data[0][this.klass.DATA_VALUES_INDEX].length * 2), 7.0);
if (!this.hide_lines && prev_x !== null && prev_y !== null) {
this._d.line(prev_x, prev_y, new_x, new_y);
} else if (this._one_point) {
// Show a circle if there's just one point
this._d.circle(new_x, new_y, new_x - circle_radius, new_y);
}
if (!this.hide_dots) this._d.circle(new_x, new_y, new_x - circle_radius, new_y);
this._draw_tooltip(new_x - circle_radius, new_y - circle_radius,
2 * circle_radius, 2 *circle_radius,
data_row[this.klass.DATA_LABEL_INDEX],
data_row[this.klass.DATA_COLOR_INDEX],
raw_data[index], index);
prev_x = new_x;
prev_y = new_y;
}, this);
}, this);
},
_normalize: function() {
this.maximum_value = Math.max(this.maximum_value, this.baseline_value);
this.callSuper();
if (this.baseline_value !== null) this._norm_baseline = this.baseline_value / this.maximum_value;
},
_contains_one_point_only: function(data_row) {
// Spin through data to determine if there is just one value present.
var count = 0;
Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point) {
if (data_point !== undefined) count += 1;
});
return count === 1;
}
});
// Graph with dots and labels along a vertical access
// see: 'Creating More Effective Graphs' by Robbins
Bluff.Dot = new JS.Class(Bluff.Base, {
draw: function() {
this.has_left_labels = true;
this.callSuper();
if (!this._has_data) return;
// Setup spacing.
//
var spacing_factor = 1.0;
this._items_width = this._graph_height / this._column_count;
this._item_width = this._items_width * spacing_factor / this._norm_data.length;
this._d.stroke_opacity = 0.0;
var height = Bluff.array_new(this._column_count, 0),
length = Bluff.array_new(this._column_count, this._graph_left),
padding = (this._items_width * (1 - spacing_factor)) / 2;
Bluff.each(this._norm_data, function(data_row, row_index) {
Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, point_index) {
var x_pos = this._graph_left + (data_point * this._graph_width) - Math.round(this._item_width/6.0);
var y_pos = this._graph_top + (this._items_width * point_index) + padding + Math.round(this._item_width/2.0);
if (row_index === 0) {
this._d.stroke = this.marker_color;
this._d.stroke_width = 1.0;
this._d.opacity = 0.1;
this._d.line(this._graph_left, y_pos, this._graph_left + this._graph_width, y_pos);
}
this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
this._d.stroke = 'transparent';
this._d.circle(x_pos, y_pos, x_pos + Math.round(this._item_width/3.0), y_pos);
// Calculate center based on item_width and current row
var label_center = this._graph_top + (this._items_width * point_index + this._items_width / 2) + padding;
this._draw_label(label_center, point_index);
}, this);
}, this);
},
// Instead of base class version, draws vertical background lines and label
_draw_line_markers: function() {
if (this.hide_line_markers) return;
this._d.stroke_antialias = false;
// Draw horizontal line markers and annotate with numbers
this._d.stroke_width = 1;
var number_of_lines = 5;
// TODO Round maximum marker value to a round number like 100, 0.1, 0.5, etc.
var increment = this._significant(this.maximum_value / number_of_lines);
for (var index = 0; index <= number_of_lines; index++) {
var line_diff = (this._graph_right - this._graph_left) / number_of_lines,
x = this._graph_right - (line_diff * index) - 1,
diff = index - number_of_lines,
marker_label = Math.abs(diff) * increment;
this._d.stroke = this.marker_color;
this._d.line(x, this._graph_bottom, x, this._graph_bottom + 0.5 * this.klass.LABEL_MARGIN);
if (!this.hide_line_numbers) {
this._d.fill = this.font_color;
if (this.font) this._d.font = this.font;
this._d.stroke = 'transparent';
this._d.pointsize = this._scale_fontsize(this.marker_font_size);
this._d.gravity = 'center';
// TODO Center text over line
this._d.annotate_scaled(0, 0, // Width of box to draw text in
x, this._graph_bottom + (this.klass.LABEL_MARGIN * 2.0), // Coordinates of text
marker_label, this._scale);
}
this._d.stroke_antialias = true;
}
},
// Draw on the Y axis instead of the X
_draw_label: function(y_offset, index) {
if (this.labels[index] && !this._labels_seen[index]) {
this._d.fill = this.font_color;
if (this.font) this._d.font = this.font;
this._d.stroke = 'transparent';
this._d.font_weight = 'normal';
this._d.pointsize = this._scale_fontsize(this.marker_font_size);
this._d.gravity = 'east';
this._d.annotate_scaled(1, 1,
this._graph_left - this.klass.LABEL_MARGIN * 2.0, y_offset,
this.labels[index], this._scale);
this._labels_seen[index] = true;
}
}
});
// Experimental!!! See also the Spider graph.
Bluff.Net = new JS.Class(Bluff.Base, {
// Hide parts of the graph to fit more datapoints, or for a different appearance.
hide_dots: null,
//Dimensions of lines and dots; calculated based on dataset size if left unspecified
line_width: null,
dot_radius: null,
initialize: function() {
this.callSuper();
this.hide_dots = false;
this.hide_line_numbers = true;
},
draw: function() {
this.callSuper();
if (!this._has_data) return;
this._radius = this._graph_height / 2.0;
this._center_x = this._graph_left + (this._graph_width / 2.0);
this._center_y = this._graph_top + (this._graph_height / 2.0) - 10; // Move graph up a bit
this._x_increment = this._graph_width / (this._column_count - 1);
var circle_radius = this.dot_radius ||
this._clip_value_if_greater_than(this._columns / (this._norm_data[0][this.klass.DATA_VALUES_INDEX].length * 2.5), 7.0);
this._d.stroke_opacity = 1.0;
this._d.stroke_width = this.line_width ||
this._clip_value_if_greater_than(this._columns / (this._norm_data[0][this.klass.DATA_VALUES_INDEX].length * 4), 3.0);
var level;
if (this._norm_baseline !== undefined) {
level = this._graph_top + (this._graph_height - this._norm_baseline * this._graph_height);
this._d.push();
this._d.stroke_color = this.baseline_color;
this._d.fill_opacity = 0.0;
// this._d.stroke_dasharray(10, 20);
this._d.stroke_width = 5;
this._d.line(this._graph_left, level, this._graph_left + this._graph_width, level);
this._d.pop();
}
Bluff.each(this._norm_data, function(data_row) {
var prev_x = null, prev_y = null;
Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, index) {
if (data_point === undefined) return;
var rad_pos = index * Math.PI * 2 / this._column_count,
point_distance = data_point * this._radius,
start_x = this._center_x + Math.sin(rad_pos) * point_distance,
start_y = this._center_y - Math.cos(rad_pos) * point_distance,
next_index = (index + 1 < data_row[this.klass.DATA_VALUES_INDEX].length) ? index + 1 : 0,
next_rad_pos = next_index * Math.PI * 2 / this._column_count,
next_point_distance = data_row[this.klass.DATA_VALUES_INDEX][next_index] * this._radius,
end_x = this._center_x + Math.sin(next_rad_pos) * next_point_distance,
end_y = this._center_y - Math.cos(next_rad_pos) * next_point_distance;
this._d.stroke = data_row[this.klass.DATA_COLOR_INDEX];
this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
this._d.line(start_x, start_y, end_x, end_y);
if (!this.hide_dots) this._d.circle(start_x, start_y, start_x - circle_radius, start_y);
}, this);
}, this);
},
// the lines connecting in the center, with the first line vertical
_draw_line_markers: function() {
if (this.hide_line_markers) return;
// have to do this here (AGAIN)... see draw() in this class
// because this funtion is called before the @radius, @center_x and @center_y are set
this._radius = this._graph_height / 2.0;
this._center_x = this._graph_left + (this._graph_width / 2.0);
this._center_y = this._graph_top + (this._graph_height / 2.0) - 10; // Move graph up a bit
var rad_pos, marker_label;
for (var index = 0, n = this._column_count; index < n; index++) {
rad_pos = index * Math.PI * 2 / this._column_count;
// Draw horizontal line markers and annotate with numbers
this._d.stroke = this.marker_color;
this._d.stroke_width = 1;
this._d.line(this._center_x, this._center_y, this._center_x + Math.sin(rad_pos) * this._radius, this._center_y - Math.cos(rad_pos) * this._radius);
marker_label = this.labels[index] ? this.labels[index] : '000';
this._draw_label(this._center_x, this._center_y, rad_pos * 360 / (2 * Math.PI), this._radius, marker_label);
}
},
_draw_label: function(center_x, center_y, angle, radius, amount) {
var r_offset = 1.1,
x_offset = center_x, // + 15 // The label points need to be tweaked slightly
y_offset = center_y, // + 0 // This one doesn't though
rad_pos = angle * Math.PI / 180,
x = x_offset + (radius * r_offset * Math.sin(rad_pos)),
y = y_offset - (radius * r_offset * Math.cos(rad_pos));
// Draw label
this._d.fill = this.marker_color;
if (this.font) this._d.font = this.font;
this._d.pointsize = this._scale_fontsize(20);
this._d.stroke = 'transparent';
this._d.font_weight = 'bold';
this._d.gravity = 'center';
this._d.annotate_scaled(0, 0, x, y, amount, this._scale);
}
});
// Here's how to make a Pie graph:
//
// g = new Bluff.Pie('canvasId');
// g.title = "Visual Pie Graph Test";
// g.data('Fries', 20);
// g.data('Hamburgers', 50);
// g.draw();
//
// To control where the pie chart starts creating slices, use #zero_degree.
Bluff.Pie = new JS.Class(Bluff.Base, {
extend: {
TEXT_OFFSET_PERCENTAGE: 0.08
},
// Can be used to make the pie start cutting slices at the top (-90.0)
// or at another angle. Default is 0.0, which starts at 3 o'clock.
zero_degreee: null,
// Do not show labels for slices that are less than this percent. Use 0 to always show all labels.
hide_labels_less_than: null,
initialize_ivars: function() {
this.callSuper();
this.zero_degree = 0.0;
this.hide_labels_less_than = 0.0;
},
draw: function() {
this.hide_line_markers = true;
this.callSuper();
if (!this._has_data) return;
var diameter = this._graph_height,
radius = (Math.min(this._graph_width, this._graph_height) / 2.0) * 0.8,
top_x = this._graph_left + (this._graph_width - diameter) / 2.0,
center_x = this._graph_left + (this._graph_width / 2.0),
center_y = this._graph_top + (this._graph_height / 2.0) - 10, // Move graph up a bit
total_sum = this._sums_for_pie(),
prev_degrees = this.zero_degree,
index = this.klass.DATA_VALUES_INDEX;
// Use full data since we can easily calculate percentages
if (this.sort) this._data.sort(function(a,b) { return a[index][0] - b[index][0]; });
Bluff.each(this._data, function(data_row, i) {
if (data_row[this.klass.DATA_VALUES_INDEX][0] > 0) {
this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
var current_degrees = (data_row[this.klass.DATA_VALUES_INDEX][0] / total_sum) * 360;
// Gruff uses ellipse() here, but canvas doesn't seem to support it.
// circle() is fine for our purposes here.
this._d.circle(center_x, center_y,
center_x + radius, center_y,
prev_degrees, prev_degrees + current_degrees + 0.5); // <= +0.5 'fudge factor' gets rid of the ugly gaps
var half_angle = prev_degrees + ((prev_degrees + current_degrees) - prev_degrees) / 2,
label_val = Math.round((data_row[this.klass.DATA_VALUES_INDEX][0] / total_sum) * 100.0),
label_string;
if (label_val >= this.hide_labels_less_than) {
label_string = this._label(data_row[this.klass.DATA_VALUES_INDEX][0]);
this._draw_label(center_x, center_y, half_angle,
radius + (radius * this.klass.TEXT_OFFSET_PERCENTAGE),
label_string,
data_row, i);
}
prev_degrees += current_degrees;
}
}, this);
// TODO debug a circle where the text is drawn...
},
// Labels are drawn around a slightly wider ellipse to give room for
// labels on the left and right.
_draw_label: function(center_x, center_y, angle, radius, amount, data_row, i) {
// TODO Don't use so many hard-coded numbers
var r_offset = 20.0, // The distance out from the center of the pie to get point
x_offset = center_x, // + 15.0 # The label points need to be tweaked slightly
y_offset = center_y, // This one doesn't though
radius_offset = radius + r_offset,
ellipse_factor = radius_offset * 0.15,
x = x_offset + ((radius_offset + ellipse_factor) * Math.cos(angle * Math.PI/180)),
y = y_offset + (radius_offset * Math.sin(angle * Math.PI/180));
// Draw label
this._d.fill = this.font_color;
if (this.font) this._d.font = this.font;
this._d.pointsize = this._scale_fontsize(this.marker_font_size);
this._d.font_weight = 'bold';
this._d.gravity = 'center';
this._d.annotate_scaled(0,0, x,y, amount, this._scale);
this._draw_tooltip(x - 20, y - 20, 40, 40,
data_row[this.klass.DATA_LABEL_INDEX],
data_row[this.klass.DATA_COLOR_INDEX],
amount, i);
},
_sums_for_pie: function() {
var total_sum = 0;
Bluff.each(this._data, function(data_row) {
total_sum += data_row[this.klass.DATA_VALUES_INDEX][0];
}, this);
return total_sum;
}
});
// Graph with individual horizontal bars instead of vertical bars.
Bluff.SideBar = new JS.Class(Bluff.Base, {
// Spacing factor applied between bars
bar_spacing: 0.9,
draw: function() {
this.has_left_labels = true;
this.callSuper();
if (!this._has_data) return;
this._draw_bars();
},
_draw_bars: function() {
this._bars_width = this._graph_height / this._column_count;
this._bar_width = this._bars_width / this._norm_data.length;
this._d.stroke_opacity = 0.0;
var height = Bluff.array_new(this._column_count, 0),
length = Bluff.array_new(this._column_count, this._graph_left),
padding = (this._bar_width * (1 - this.bar_spacing)) / 2;
Bluff.each(this._norm_data, function(data_row, row_index) {
var raw_data = this._data[row_index][this.klass.DATA_VALUES_INDEX];
Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, point_index) {
// Using the original calcs from the stacked bar chart
// to get the difference between
// part of the bart chart we wish to stack.
var temp1 = this._graph_left + (this._graph_width - data_point * this._graph_width - height[point_index]),
temp2 = this._graph_left + this._graph_width - height[point_index],
difference = temp2 - temp1,
left_x = length[point_index] - 1,
left_y = this._graph_top + (this._bars_width * point_index) + (this._bar_width * row_index) + padding,
right_x = left_x + difference,
right_y = left_y + this._bar_width * this.bar_spacing;
height[point_index] += (data_point * this._graph_width);
this._d.stroke = 'transparent';
this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
this._d.rectangle(left_x, left_y, right_x, right_y);
this._draw_tooltip(left_x, left_y,
right_x - left_x, right_y - left_y,
data_row[this.klass.DATA_LABEL_INDEX],
data_row[this.klass.DATA_COLOR_INDEX],
raw_data[point_index], point_index);
// Calculate center based on bar_width and current row
var label_center = this._graph_top + (this._bars_width * point_index + this._bars_width / 2);
this._draw_label(label_center, point_index);
}, this)
}, this);
},
// Instead of base class version, draws vertical background lines and label
_draw_line_markers: function() {
if (this.hide_line_markers) return;
this._d.stroke_antialias = false;
// Draw horizontal line markers and annotate with numbers
this._d.stroke_width = 1;
var number_of_lines = 5;
// TODO Round maximum marker value to a round number like 100, 0.1, 0.5, etc.
var increment = this._significant(this._spread / number_of_lines),
line_diff, x, diff, marker_label;
for (var index = 0; index <= number_of_lines; index++) {
line_diff = (this._graph_right - this._graph_left) / number_of_lines;
x = this._graph_right - (line_diff * index) - 1;
diff = index - number_of_lines;
marker_label = Math.abs(diff) * increment + this.minimum_value;
this._d.stroke = this.marker_color;
this._d.line(x, this._graph_bottom, x, this._graph_top);
if (!this.hide_line_numbers) {
this._d.fill = this.font_color;
if (this.font) this._d.font = this.font;
this._d.stroke = 'transparent';
this._d.pointsize = this._scale_fontsize(this.marker_font_size);
this._d.gravity = 'center';
// TODO Center text over line
this._d.annotate_scaled(
0, 0, // Width of box to draw text in
x, this._graph_bottom + (this.klass.LABEL_MARGIN * 2.0), // Coordinates of text
this._label(marker_label), this._scale);
}
}
},
// Draw on the Y axis instead of the X
_draw_label: function(y_offset, index) {
if (this.labels[index] && !this._labels_seen[index]) {
this._d.fill = this.font_color;
if (this.font) this._d.font = this.font;
this._d.stroke = 'transparent';
this._d.font_weight = 'normal';
this._d.pointsize = this._scale_fontsize(this.marker_font_size);
this._d.gravity = 'east';
this._d.annotate_scaled(1, 1,
this._graph_left - this.klass.LABEL_MARGIN * 2.0, y_offset,
this.labels[index], this._scale);
this._labels_seen[index] = true;
}
}
});
// Experimental!!! See also the Net graph.
//
// Submitted by Kevin Clark http://glu.ttono.us/
Bluff.Spider = new JS.Class(Bluff.Base, {
// Hide all text
hide_text: null,
hide_axes: null,
transparent_background: null,
initialize: function(renderer, max_value, target_width) {
this.callSuper(renderer, target_width);
this._max_value = max_value;
this.hide_legend = true;
},
draw: function() {
this.hide_line_markers = true;
this.callSuper();
if (!this._has_data) return;
// Setup basic positioning
var diameter = this._graph_height,
radius = this._graph_height / 2.0,
top_x = this._graph_left + (this._graph_width - diameter) / 2.0,
center_x = this._graph_left + (this._graph_width / 2.0),
center_y = this._graph_top + (this._graph_height / 2.0) - 25; // Move graph up a bit
this._unit_length = radius / this._max_value;
var total_sum = this._sums_for_spider(),
prev_degrees = 0.0,
additive_angle = (2 * Math.PI) / this._data.length,
current_angle = 0.0;
// Draw axes
if (!this.hide_axes) this._draw_axes(center_x, center_y, radius, additive_angle);
// Draw polygon
this._draw_polygon(center_x, center_y, additive_angle);
},
_normalize_points: function(value) {
return value * this._unit_length;
},
_draw_label: function(center_x, center_y, angle, radius, amount) {
var r_offset = 50, // The distance out from the center of the pie to get point
x_offset = center_x, // The label points need to be tweaked slightly
y_offset = center_y + 0, // This one doesn't though
x = x_offset + ((radius + r_offset) * Math.cos(angle)),
y = y_offset + ((radius + r_offset) * Math.sin(angle));
// Draw label
this._d.fill = this.marker_color;
if (this.font) this._d.font = this.font;
this._d.pointsize = this._scale_fontsize(this.legend_font_size);
this._d.stroke = 'transparent';
this._d.font_weight = 'bold';
this._d.gravity = 'center';
this._d.annotate_scaled(0, 0,
x, y,
amount, this._scale);
},
_draw_axes: function(center_x, center_y, radius, additive_angle, line_color) {
if (this.hide_axes) return;
var current_angle = 0.0;
Bluff.each(this._data, function(data_row) {
this._d.stroke = line_color || data_row[this.klass.DATA_COLOR_INDEX];
this._d.stroke_width = 5.0;
var x_offset = radius * Math.cos(current_angle);
var y_offset = radius * Math.sin(current_angle);
this._d.line(center_x, center_y,
center_x + x_offset,
center_y + y_offset);
if (!this.hide_text) this._draw_label(center_x, center_y, current_angle, radius, data_row[this.klass.DATA_LABEL_INDEX]);
current_angle += additive_angle;
}, this);
},
_draw_polygon: function(center_x, center_y, additive_angle, color) {
var points = [],
current_angle = 0.0;
Bluff.each(this._data, function(data_row) {
points.push(center_x + this._normalize_points(data_row[this.klass.DATA_VALUES_INDEX][0]) * Math.cos(current_angle));
points.push(center_y + this._normalize_points(data_row[this.klass.DATA_VALUES_INDEX][0]) * Math.sin(current_angle));
current_angle += additive_angle;
}, this);
this._d.stroke_width = 1.0;
this._d.stroke = color || this.marker_color;
this._d.fill = color || this.marker_color;
this._d.fill_opacity = 0.4;
this._d.polyline(points);
},
_sums_for_spider: function() {
var sum = 0.0;
Bluff.each(this._data, function(data_row) {
sum += data_row[this.klass.DATA_VALUES_INDEX][0];
}, this);
return sum;
}
});
// Used by StackedBar and child classes.
Bluff.Base.StackedMixin = new JS.Module({
// Get sum of each stack
_get_maximum_by_stack: function() {
var max_hash = {};
Bluff.each(this._data, function(data_set) {
Bluff.each(data_set[this.klass.DATA_VALUES_INDEX], function(data_point, i) {
if (!max_hash[i]) max_hash[i] = 0.0;
max_hash[i] += data_point;
}, this);
}, this);
// this.maximum_value = 0;
for (var key in max_hash) {
if (max_hash[key] > this.maximum_value) this.maximum_value = max_hash[key];
}
this.minimum_value = 0;
}
});
Bluff.StackedArea = new JS.Class(Bluff.Base, {
include: Bluff.Base.StackedMixin,
last_series_goes_on_bottom: null,
draw: function() {
this._get_maximum_by_stack();
this.callSuper();
if (!this._has_data) return;
this._x_increment = this._graph_width / (this._column_count - 1);
this._d.stroke = 'transparent';
var height = Bluff.array_new(this._column_count, 0);
var data_points = null;
var iterator = this.last_series_goes_on_bottom ? 'reverse_each' : 'each';
Bluff[iterator](this._norm_data, function(data_row) {
var prev_data_points = data_points;
data_points = [];
Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, index) {
// Use incremented x and scaled y
var new_x = this._graph_left + (this._x_increment * index);
var new_y = this._graph_top + (this._graph_height - data_point * this._graph_height - height[index]);
height[index] += (data_point * this._graph_height);
data_points.push(new_x);
data_points.push(new_y);
this._draw_label(new_x, index);
}, this);
var poly_points, i, n;
if (prev_data_points) {
poly_points = Bluff.array(data_points);
for (i = prev_data_points.length/2 - 1; i >= 0; i--) {
poly_points.push(prev_data_points[2*i]);
poly_points.push(prev_data_points[2*i+1]);
}
poly_points.push(data_points[0]);
poly_points.push(data_points[1]);
} else {
poly_points = Bluff.array(data_points);
poly_points.push(this._graph_right);
poly_points.push(this._graph_bottom - 1);
poly_points.push(this._graph_left);
poly_points.push(this._graph_bottom - 1);
poly_points.push(data_points[0]);
poly_points.push(data_points[1]);
}
this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
this._d.polyline(poly_points);
}, this);
}
});
Bluff.StackedBar = new JS.Class(Bluff.Base, {
include: Bluff.Base.StackedMixin,
// Spacing factor applied between bars
bar_spacing: 0.9,
// Draws a bar graph, but multiple sets are stacked on top of each other.
draw: function() {
this._get_maximum_by_stack();
this.callSuper();
if (!this._has_data) return;
this._bar_width = this._graph_width / this._column_count;
var padding = (this._bar_width * (1 - this.bar_spacing)) / 2;
this._d.stroke_opacity = 0.0;
var height = Bluff.array_new(this._column_count, 0);
Bluff.each(this._norm_data, function(data_row, row_index) {
var raw_data = this._data[row_index][this.klass.DATA_VALUES_INDEX];
Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, point_index) {
// Calculate center based on bar_width and current row
var label_center = this._graph_left + (this._bar_width * point_index) + (this._bar_width * this.bar_spacing / 2.0);
this._draw_label(label_center, point_index);
if (data_point == 0) return;
// Use incremented x and scaled y
var left_x = this._graph_left + (this._bar_width * point_index) + padding;
var left_y = this._graph_top + (this._graph_height -
data_point * this._graph_height -
height[point_index]) + 1;
var right_x = left_x + this._bar_width * this.bar_spacing;
var right_y = this._graph_top + this._graph_height - height[point_index] - 1;
// update the total height of the current stacked bar
height[point_index] += (data_point * this._graph_height);
this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
this._d.rectangle(left_x, left_y, right_x, right_y);
this._draw_tooltip(left_x, left_y,
right_x - left_x, right_y - left_y,
data_row[this.klass.DATA_LABEL_INDEX],
data_row[this.klass.DATA_COLOR_INDEX],
raw_data[point_index], point_index);
}, this);
}, this);
}
});
// A special bar graph that shows a single dataset as a set of
// stacked bars. The bottom bar shows the running total and
// the top bar shows the new value being added to the array.
Bluff.AccumulatorBar = new JS.Class(Bluff.StackedBar, {
draw: function() {
if (this._data.length !== 1) throw 'Incorrect number of datasets';
var accumulator_array = [],
index = 0,
increment_array = [];
Bluff.each(this._data[0][this.klass.DATA_VALUES_INDEX], function(value) {
var max = -Infinity;
Bluff.each(increment_array, function(x) { max = Math.max(max, x); });
increment_array.push((index > 0) ? (value + max) : value);
accumulator_array.push(increment_array[index] - value);
index += 1;
}, this);
this.data("Accumulator", accumulator_array);
this.callSuper();
}
});
// New gruff graph type added to enable sideways stacking bar charts
// (basically looks like a x/y flip of a standard stacking bar chart)
//
// alun.eyre@googlemail.com
Bluff.SideStackedBar = new JS.Class(Bluff.SideBar, {
include: Bluff.Base.StackedMixin,
// Spacing factor applied between bars
bar_spacing: 0.9,
draw: function() {
this.has_left_labels = true;
this._get_maximum_by_stack();
this.callSuper();
},
_draw_bars: function() {
this._bar_width = this._graph_height / this._column_count;
var height = Bluff.array_new(this._column_count, 0),
length = Bluff.array_new(this._column_count, this._graph_left),
padding = (this._bar_width * (1 - this.bar_spacing)) / 2;
Bluff.each(this._norm_data, function(data_row, row_index) {
var raw_data = this._data[row_index][this.klass.DATA_VALUES_INDEX];
Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, point_index) {
// using the original calcs from the stacked bar chart to get the difference between
// part of the bart chart we wish to stack.
var temp1 = this._graph_left + (this._graph_width -
data_point * this._graph_width -
height[point_index]) + 1;
var temp2 = this._graph_left + this._graph_width - height[point_index] - 1;
var difference = temp2 - temp1;
this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
var left_x = length[point_index], //+ 1
left_y = this._graph_top + (this._bar_width * point_index) + padding,
right_x = left_x + difference,
right_y = left_y + this._bar_width * this.bar_spacing;
length[point_index] += difference;
height[point_index] += (data_point * this._graph_width - 2);
this._d.rectangle(left_x, left_y, right_x, right_y);
this._draw_tooltip(left_x, left_y,
right_x - left_x, right_y - left_y,
data_row[this.klass.DATA_LABEL_INDEX],
data_row[this.klass.DATA_COLOR_INDEX],
raw_data[point_index], point_index);
// Calculate center based on bar_width and current row
var label_center = this._graph_top + (this._bar_width * point_index) + (this._bar_width * this.bar_spacing / 2.0);
this._draw_label(label_center, point_index);
}, this);
}, this);
},
_larger_than_max: function(data_point, index) {
index = index || 0;
return this._max(data_point, index) > this.maximum_value;
},
_max: function(data_point, index) {
var sum = 0;
Bluff.each(this._data, function(item) {
sum += item[this.klass.DATA_VALUES_INDEX][index];
}, this);
return sum;
}
});
Bluff.Mini.Legend = new JS.Module({
hide_mini_legend: false,
// The canvas needs to be bigger so we can put the legend beneath it.
_expand_canvas_for_vertical_legend: function() {
if (this.hide_mini_legend) return;
this._legend_labels = Bluff.map(this._data, function(item) {
return item[this.klass.DATA_LABEL_INDEX];
}, this);
var legend_height = this._scale_fontsize(
this._data.length * this._calculate_line_height() +
this.top_margin + this.bottom_margin);
this._original_rows = this._raw_rows;
this._original_columns = this._raw_columns;
switch (this.legend_position) {
case 'right':
this._rows = Math.max(this._rows, legend_height);
this._columns += this._calculate_legend_width() + this.left_margin;
break;
default:
this._rows += legend_height;
break;
}
this._render_background();
},
_calculate_line_height: function() {
return this._calculate_caps_height(this.legend_font_size) * 1.7;
},
_calculate_legend_width: function() {
var width = 0;
Bluff.each(this._legend_labels, function(label) {
width = Math.max(this._calculate_width(this.legend_font_size, label), width);
}, this);
return this._scale_fontsize(width + 40*1.7);
},
// Draw the legend beneath the existing graph.
_draw_vertical_legend: function() {
if (this.hide_mini_legend) return;
var legend_square_width = 40.0, // small square with color of this item
legend_square_margin = 10.0,
legend_left_margin = 100.0,
legend_top_margin = 40.0;
// May fix legend drawing problem at small sizes
if (this.font) this._d.font = this.font;
this._d.pointsize = this.legend_font_size;
var current_x_offset, current_y_offset;
switch (this.legend_position) {
case 'right':
current_x_offset = this._original_columns + this.left_margin;
current_y_offset = this.top_margin + legend_top_margin;
break;
default:
current_x_offset = legend_left_margin,
current_y_offset = this._original_rows + legend_top_margin;
break;
}
this._debug(function() {
this._d.line(0.0, current_y_offset, this._raw_columns, current_y_offset);
});
Bluff.each(this._legend_labels, function(legend_label, index) {
// Draw label
this._d.fill = this.font_color;
if (this.font) this._d.font = this.font;
this._d.pointsize = this._scale_fontsize(this.legend_font_size);
this._d.stroke = 'transparent';
this._d.font_weight = 'normal';
this._d.gravity = 'west';
this._d.annotate_scaled(this._raw_columns, 1.0,
current_x_offset + (legend_square_width * 1.7), current_y_offset,
this._truncate_legend_label(legend_label), this._scale);
// Now draw box with color of this dataset
this._d.stroke = 'transparent';
this._d.fill = this._data[index][this.klass.DATA_COLOR_INDEX];
this._d.rectangle(current_x_offset,
current_y_offset - legend_square_width / 2.0,
current_x_offset + legend_square_width,
current_y_offset + legend_square_width / 2.0);
current_y_offset += this._calculate_line_height();
}, this);
this._color_index = 0;
},
// Shorten long labels so they will fit on the canvas.
_truncate_legend_label: function(label) {
var truncated_label = String(label);
while (this._calculate_width(this._scale_fontsize(this.legend_font_size), truncated_label) > (this._columns - this.legend_left_margin - this.right_margin) && (truncated_label.length > 1))
truncated_label = truncated_label.substr(0, truncated_label.length-1);
return truncated_label + (truncated_label.length < String(label).length ? "..." : '');
}
});
// Makes a small bar graph suitable for display at 200px or even smaller.
//
Bluff.Mini.Bar = new JS.Class(Bluff.Bar, {
include: Bluff.Mini.Legend,
initialize_ivars: function() {
this.callSuper();
this.hide_legend = true;
this.hide_title = true;
this.hide_line_numbers = true;
this.marker_font_size = 50.0;
this.minimum_value = 0.0;
this.maximum_value = 0.0;
this.legend_font_size = 60.0;
},
draw: function() {
this._expand_canvas_for_vertical_legend();
this.callSuper();
this._draw_vertical_legend();
}
});
// Makes a small pie graph suitable for display at 200px or even smaller.
//
Bluff.Mini.Pie = new JS.Class(Bluff.Pie, {
include: Bluff.Mini.Legend,
initialize_ivars: function() {
this.callSuper();
this.hide_legend = true;
this.hide_title = true;
this.hide_line_numbers = true;
this.marker_font_size = 60.0;
this.legend_font_size = 60.0;
},
draw: function() {
this._expand_canvas_for_vertical_legend();
this.callSuper();
this._draw_vertical_legend();
}
});
// Makes a small pie graph suitable for display at 200px or even smaller.
//
Bluff.Mini.SideBar = new JS.Class(Bluff.SideBar, {
include: Bluff.Mini.Legend,
initialize_ivars: function() {
this.callSuper();
this.hide_legend = true;
this.hide_title = true;
this.hide_line_numbers = true;
this.marker_font_size = 50.0;
this.legend_font_size = 50.0;
},
draw: function() {
this._expand_canvas_for_vertical_legend();
this.callSuper();
this._draw_vertical_legend();
}
});
Bluff.Renderer = new JS.Class({
extend: {
WRAPPER_CLASS: 'bluff-wrapper',
TEXT_CLASS: 'bluff-text',
TARGET_CLASS: 'bluff-tooltip-target'
},
font: 'Arial, Helvetica, Verdana, sans-serif',
gravity: 'north',
initialize: function(canvasId) {
this._canvas = document.getElementById(canvasId);
this._ctx = this._canvas.getContext('2d');
},
scale: function(sx, sy) {
this._sx = sx;
this._sy = sy || sx;
},
caps_height: function(font_size) {
var X = this._sized_text(font_size, 'X'),
height = this._element_size(X).height;
this._remove_node(X);
return height;
},
text_width: function(font_size, text) {
var element = this._sized_text(font_size, text);
var width = this._element_size(element).width;
this._remove_node(element);
return width;
},
get_type_metrics: function(text) {
var node = this._sized_text(this.pointsize, text);
document.body.appendChild(node);
var size = this._element_size(node);
this._remove_node(node);
return size;
},
clear: function(width, height) {
this._canvas.width = width;
this._canvas.height = height;
this._ctx.clearRect(0, 0, width, height);
var wrapper = this._text_container(), children = wrapper.childNodes, i = children.length;
wrapper.style.width = width + 'px';
wrapper.style.height = height + 'px';
while (i--) {
if (children[i].tagName.toLowerCase() !== 'canvas') {
Bluff.Event.stopObserving(children[i]);
this._remove_node(children[i]);
}
}
},
push: function() {
this._ctx.save();
},
pop: function() {
this._ctx.restore();
},
render_gradiated_background: function(width, height, top_color, bottom_color) {
this.clear(width, height);
var gradient = this._ctx.createLinearGradient(0,0, 0,height);
gradient.addColorStop(0, top_color);
gradient.addColorStop(1, bottom_color);
this._ctx.fillStyle = gradient;
this._ctx.fillRect(0, 0, width, height);
},
render_solid_background: function(width, height, color) {
this.clear(width, height);
this._ctx.fillStyle = color;
this._ctx.fillRect(0, 0, width, height);
},
annotate_scaled: function(width, height, x, y, text, scale) {
var scaled_width = (width * scale) >= 1 ? (width * scale) : 1;
var scaled_height = (height * scale) >= 1 ? (height * scale) : 1;
var text = this._sized_text(this.pointsize, text);
text.style.color = this.fill;
text.style.cursor = 'default';
text.style.fontWeight = this.font_weight;
text.style.textAlign = 'center';
text.style.left = (this._sx * x + this._left_adjustment(text, scaled_width)) + 'px';
text.style.top = (this._sy * y + this._top_adjustment(text, scaled_height)) + 'px';
},
tooltip: function(left, top, width, height, name, color, data) {
if (width < 0) left += width;
if (height < 0) top += height;
var wrapper = this._canvas.parentNode,
target = document.createElement('div');
target.className = this.klass.TARGET_CLASS;
target.style.cursor = 'default';
target.style.position = 'absolute';
target.style.left = (this._sx * left - 3) + 'px';
target.style.top = (this._sy * top - 3) + 'px';
target.style.width = (this._sx * Math.abs(width) + 5) + 'px';
target.style.height = (this._sy * Math.abs(height) + 5) + 'px';
target.style.fontSize = 0;
target.style.overflow = 'hidden';
Bluff.Event.observe(target, 'mouseover', function(node) {
Bluff.Tooltip.show(name, color, data);
});
Bluff.Event.observe(target, 'mouseout', function(node) {
Bluff.Tooltip.hide();
});
wrapper.appendChild(target);
return target;
},
circle: function(origin_x, origin_y, perim_x, perim_y, arc_start, arc_end) {
var radius = Math.sqrt(Math.pow(perim_x - origin_x, 2) + Math.pow(perim_y - origin_y, 2));
var alpha = 0, beta = 2 * Math.PI; // radians to full circle
this._ctx.fillStyle = this.fill;
this._ctx.beginPath();
if (arc_start !== undefined && arc_end !== undefined &&
Math.abs(Math.floor(arc_end - arc_start)) !== 360) {
alpha = arc_start * Math.PI/180;
beta = arc_end * Math.PI/180;
this._ctx.moveTo(this._sx * (origin_x + radius * Math.cos(beta)), this._sy * (origin_y + radius * Math.sin(beta)));
this._ctx.lineTo(this._sx * origin_x, this._sy * origin_y);
this._ctx.lineTo(this._sx * (origin_x + radius * Math.cos(alpha)), this._sy * (origin_y + radius * Math.sin(alpha)));
}
this._ctx.arc(this._sx * origin_x, this._sy * origin_y, this._sx * radius, alpha, beta, false); // draw it clockwise
this._ctx.fill();
},
line: function(sx, sy, ex, ey) {
this._ctx.strokeStyle = this.stroke;
this._ctx.lineWidth = this.stroke_width;
this._ctx.beginPath();
this._ctx.moveTo(this._sx * sx, this._sy * sy);
this._ctx.lineTo(this._sx * ex, this._sy * ey);
this._ctx.stroke();
},
polyline: function(points) {
this._ctx.fillStyle = this.fill;
this._ctx.globalAlpha = this.fill_opacity || 1;
try { this._ctx.strokeStyle = this.stroke; } catch (e) {}
var x = points.shift(), y = points.shift();
this._ctx.beginPath();
this._ctx.moveTo(this._sx * x, this._sy * y);
while (points.length > 0) {
x = points.shift(); y = points.shift();
this._ctx.lineTo(this._sx * x, this._sy * y);
}
this._ctx.fill();
},
rectangle: function(ax, ay, bx, by) {
var temp;
if (ax > bx) { temp = ax; ax = bx; bx = temp; }
if (ay > by) { temp = ay; ay = by; by = temp; }
try {
this._ctx.fillStyle = this.fill;
this._ctx.fillRect(this._sx * ax, this._sy * ay, this._sx * (bx-ax), this._sy * (by-ay));
} catch (e) {}
try {
this._ctx.strokeStyle = this.stroke;
if (this.stroke !== 'transparent')
this._ctx.strokeRect(this._sx * ax, this._sy * ay, this._sx * (bx-ax), this._sy * (by-ay));
} catch (e) {}
},
_left_adjustment: function(node, width) {
var w = this._element_size(node).width;
switch (this.gravity) {
case 'west': return 0;
case 'east': return width - w;
case 'north': case 'south': case 'center':
return (width - w) / 2;
}
},
_top_adjustment: function(node, height) {
var h = this._element_size(node).height;
switch (this.gravity) {
case 'north': return 0;
case 'south': return height - h;
case 'west': case 'east': case 'center':
return (height - h) / 2;
}
},
_text_container: function() {
var wrapper = this._canvas.parentNode;
if (wrapper.className === this.klass.WRAPPER_CLASS) return wrapper;
wrapper = document.createElement('div');
wrapper.className = this.klass.WRAPPER_CLASS;
wrapper.style.position = 'relative';
wrapper.style.border = 'none';
wrapper.style.padding = '0 0 0 0';
this._canvas.parentNode.insertBefore(wrapper, this._canvas);
wrapper.appendChild(this._canvas);
return wrapper;
},
_sized_text: function(size, content) {
var text = this._text_node(content);
text.style.fontFamily = this.font;
text.style.fontSize = (typeof size === 'number') ? size + 'px' : size;
return text;
},
_text_node: function(content) {
var div = document.createElement('div');
div.className = this.klass.TEXT_CLASS;
div.style.position = 'absolute';
div.appendChild(document.createTextNode(content));
this._text_container().appendChild(div);
return div;
},
_remove_node: function(node) {
node.parentNode.removeChild(node);
if (node.className === this.klass.TARGET_CLASS)
Bluff.Event.stopObserving(node);
},
_element_size: function(element) {
var display = element.style.display;
return (display && display !== 'none')
? {width: element.offsetWidth, height: element.offsetHeight}
: {width: element.clientWidth, height: element.clientHeight};
}
});
// DOM event module, adapted from Prototype
// Copyright (c) 2005-2008 Sam Stephenson
Bluff.Event = {
_cache: [],
_isIE: (window.attachEvent && navigator.userAgent.indexOf('Opera') === -1),
observe: function(element, eventName, callback, scope) {
var handlers = Bluff.map(this._handlersFor(element, eventName),
function(entry) { return entry._handler });
if (Bluff.index(handlers, callback) !== -1) return;
var responder = function(event) {
callback.call(scope || null, element, Bluff.Event._extend(event));
};
this._cache.push({_node: element, _name: eventName,
_handler: callback, _responder: responder});
if (element.addEventListener)
element.addEventListener(eventName, responder, false);
else
element.attachEvent('on' + eventName, responder);
},
stopObserving: function(element) {
var handlers = element ? this._handlersFor(element) : this._cache;
Bluff.each(handlers, function(entry) {
if (entry._node.removeEventListener)
entry._node.removeEventListener(entry._name, entry._responder, false);
else
entry._node.detachEvent('on' + entry._name, entry._responder);
});
},
_handlersFor: function(element, eventName) {
var results = [];
Bluff.each(this._cache, function(entry) {
if (element && entry._node !== element) return;
if (eventName && entry._name !== eventName) return;
results.push(entry);
});
return results;
},
_extend: function(event) {
if (!this._isIE) return event;
if (!event) return false;
if (event._extendedByBluff) return event;
event._extendedByBluff = true;
var pointer = this._pointer(event);
event.target = event.srcElement;
event.pageX = pointer.x;
event.pageY = pointer.y;
return event;
},
_pointer: function(event) {
var docElement = document.documentElement,
body = document.body || { scrollLeft: 0, scrollTop: 0 };
return {
x: event.pageX || (event.clientX +
(docElement.scrollLeft || body.scrollLeft) -
(docElement.clientLeft || 0)),
y: event.pageY || (event.clientY +
(docElement.scrollTop || body.scrollTop) -
(docElement.clientTop || 0))
};
}
};
if (Bluff.Event._isIE)
window.attachEvent('onunload', function() {
Bluff.Event.stopObserving();
Bluff.Event._cache = null;
});
if (navigator.userAgent.indexOf('AppleWebKit/') > -1)
window.addEventListener('unload', function() {}, false);
Bluff.Tooltip = new JS.Singleton({
LEFT_OFFSET: 20,
TOP_OFFSET: -6,
DATA_LENGTH: 8,
CLASS_NAME: 'bluff-tooltip',
setup: function() {
this._tip = document.createElement('div');
this._tip.className = this.CLASS_NAME;
this._tip.style.position = 'absolute';
this.hide();
document.body.appendChild(this._tip);
Bluff.Event.observe(document.body, 'mousemove', function(body, event) {
this._tip.style.left = (event.pageX + this.LEFT_OFFSET) + 'px';
this._tip.style.top = (event.pageY + this.TOP_OFFSET) + 'px';
}, this);
},
show: function(name, color, data) {
data = Number(String(data).substr(0, this.DATA_LENGTH));
this._tip.innerHTML = '<span class="color" style="background: ' + color + ';">&nbsp;</span> ' +
'<span class="label">' + name + '</span> ' +
'<span class="data">' + data + '</span>';
this._tip.style.display = '';
},
hide: function() {
this._tip.style.display = 'none';
}
});
Bluff.Event.observe(window, 'load', Bluff.Tooltip.method('setup'));
Bluff.TableReader = new JS.Class({
NUMBER_FORMAT: /\-?(0|[1-9]\d*)(\.\d+)?(e[\+\-]?\d+)?/i,
initialize: function(table, options) {
this._options = options || {};
this._orientation = this._options.orientation || 'auto';
this._table = (typeof table === 'string')
? document.getElementById(table)
: table;
},
// Get array of data series from the table
get_data: function() {
if (!this._data) this._read();
return this._data;
},
// Get set of axis labels to use for the graph
get_labels: function() {
if (!this._labels) this._read();
return this._labels;
},
// Get the title from the table's caption
get_title: function() {
return this._title;
},
// Return series number i
get_series: function(i) {
if (this._data[i]) return this._data[i];
return this._data[i] = {points: []};
},
// Gather data by reading from the table
_read: function() {
this._row = this._col = 0;
this._row_offset = this._col_offset = 0;
this._data = [];
this._labels = {};
this._row_headings = [];
this._col_headings = [];
this._skip_rows = [];
this._skip_cols = [];
this._walk(this._table);
this._cleanup();
this._orient();
Bluff.each(this._col_headings, function(heading, i) {
this.get_series(i - this._col_offset).name = heading;
}, this);
Bluff.each(this._row_headings, function(heading, i) {
this._labels[i - this._row_offset] = heading;
}, this);
},
// Walk the table's DOM tree
_walk: function(node) {
this._visit(node);
var i, children = node.childNodes, n = children.length;
for (i = 0; i < n; i++) this._walk(children[i]);
},
// Read a single DOM node from the table
_visit: function(node) {
if (!node.tagName) return;
var content = this._strip_tags(node.innerHTML), x, y;
switch (node.tagName.toUpperCase()) {
case 'TR':
if (!this._has_data) this._row_offset = this._row;
this._row += 1;
this._col = 0;
break;
case 'TD':
if (!this._has_data) this._col_offset = this._col;
this._has_data = true;
this._col += 1;
content = content.match(this.NUMBER_FORMAT);
if (content === null) {
this.get_series(x).points[y] = null;
} else {
x = this._col - this._col_offset - 1;
y = this._row - this._row_offset - 1;
this.get_series(x).points[y] = parseFloat(content[0]);
}
break;
case 'TH':
this._col += 1;
if (this._ignore(node)) {
this._skip_cols.push(this._col);
this._skip_rows.push(this._row);
}
if (this._col === 1 && this._row === 1)
this._row_headings[0] = this._col_headings[0] = content;
else if (node.scope === "row" || this._col === 1)
this._row_headings[this._row - 1] = content;
else
this._col_headings[this._col - 1] = content;
break;
case 'CAPTION':
this._title = content;
break;
}
},
_ignore: function(node) {
if (!this._options.except) return false;
var content = this._strip_tags(node.innerHTML),
classes = (node.className || '').split(/\s+/),
list = [].concat(this._options.except);
if (Bluff.index(list, content) >= 0) return true;
var i = classes.length;
while (i--) {
if (Bluff.index(list, classes[i]) >= 0) return true;
}
return false;
},
_cleanup: function() {
var i = this._skip_cols.length, index;
while (i--) {
index = this._skip_cols[i];
if (index <= this._col_offset) continue;
this._col_headings.splice(index - 1, 1);
if (index >= this._col_offset)
this._data.splice(index - 1 - this._col_offset, 1);
}
var i = this._skip_rows.length, index;
while (i--) {
index = this._skip_rows[i];
if (index <= this._row_offset) continue;
this._row_headings.splice(index - 1, 1);
Bluff.each(this._data, function(series) {
if (index >= this._row_offset)
series.points.splice(index - 1 - this._row_offset, 1);
}, this);
}
},
_orient: function() {
switch (this._orientation) {
case 'auto':
if ((this._row_headings.length > 1 && this._col_headings.length === 1) ||
this._row_headings.length < this._col_headings.length) {
this._transpose();
}
break;
case 'rows':
this._transpose();
break;
}
},
// Transpose data in memory
_transpose: function() {
var data = this._data, tmp;
this._data = [];
Bluff.each(data, function(row, i) {
Bluff.each(row.points, function(point, p) {
this.get_series(p).points[i] = point;
}, this);
}, this);
tmp = this._row_headings;
this._row_headings = this._col_headings;
this._col_headings = tmp;
tmp = this._row_offset;
this._row_offset = this._col_offset;
this._col_offset = tmp;
},
// Remove HTML from a string
_strip_tags: function(string) {
return string.replace(/<\/?[^>]+>/gi, '');
},
extend: {
Mixin: new JS.Module({
data_from_table: function(table, options) {
var reader = new Bluff.TableReader(table, options),
data_rows = reader.get_data();
Bluff.each(data_rows, function(row) {
this.data(row.name, row.points);
}, this);
this.labels = reader.get_labels();
this.title = reader.get_title() || this.title;
}
})
}
});
Bluff.Base.include(Bluff.TableReader.Mixin);