gchart.lua

Wed, 24 Jun 2009 19:37:41 +0100

author
Matthew Wild <mwild1@gmail.com>
date
Wed, 24 Jun 2009 19:37:41 +0100
changeset 5
b4983e638117
parent 4
e17867506327
child 6
8d4be5429414
permissions
-rw-r--r--

Support for auto-scaling of data


module(..., package.seeall);

local chart = {};
chart.__index = chart;

local writers = {}; -- Table of functions which build the URL

-- Defaults
chart.base_url = "http://chart.apis.google.com/chart";
chart.width, chart.height = 320, 200;
chart.marker_color = "4D89F9";
chart.auto_scale_factor = 0.25;

-- Helpers
local function urlencode(s) return s and (s:gsub("%W", function (c) return string.format("%%%02x", c:byte()); end)); end
local typemap = { line = "lc", sparkline = "ls", plot = "lxy", bar = "bvs" };

function new(type)
	local chart_obj = {
			type = typemap[type] or type or "lc";
			series = {};
			axes = {};
			markers = {};
			scale = { min = true, max = true };
		};
	
	return setmetatable(chart_obj, chart);
end

-- Library methods --

function set_base_url(url)
	chart.base_url = url;
end

function set_default_size(width, height)
	chart.width, chart.height = width or 320, height or 200;
end

----- Chart methods -----

---- Base URL ----
function chart:set_base_url(url)
	self.base_url = url;
end

-- No writer for base URL

---- Chart type ----
function chart:set_type(type)
	self.type = typemap[type] or type;
end

-- No writer for type

---- Data series ----
function chart:add_series(data)
	table.insert(self.series, data);
end

local ee_string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.";
local ee_len = #ee_string;
local function to_extended_encoding(value)
	value = tonumber(value);
	if not value or value < 0 then return "__"; end
	local div, rem = math.floor(value/ee_len)+1, math.floor(value % ee_len)+1;
	return ee_string:sub(div, div)..ee_string:sub(rem, rem);
end

function writers:data()

	if self.scale then
		local autoscale_min, autoscale_max = (self.scale.min == true), (self.scale.max == true);
		if autoscale_min or autoscale_max then
			local min, max;
			for n, series in ipairs(self.series) do
				min, max = min or series[1], max or series[1];
				for _, value in ipairs(series) do
					if autoscale_min and value < min then min = value; end
					if autoscale_max and value > max then max = value; end
				end
			end
			if autoscale_min then
				self.scale.min = min*(1-self.auto_scale_factor);
			end
			if autoscale_max then
				self.scale.max = max*(1+self.auto_scale_factor);
			end
		end
	end

	local data = {};
	for n, series in ipairs(self.series) do
		local encoded = {};
		for _, value in ipairs(series) do
			if self.scale and value > 0 then
				--value = value - (self.scale.min or 0);
				--print(string.format("4096/(%d-%d)/(%d-%d) = %f", self.scale.max, self.scale.min, value, self.scale.min), value);
				value = 4096/((self.scale.max-self.scale.min)/(value-self.scale.min));
			end
			table.insert(encoded, to_extended_encoding(value));
		end
		table.insert(data, table.concat(encoded));
	end
	return "chd=e:"..table.concat(data, ",");
end

---- Scale ----
function chart:set_scale(min, max)
	self.scale = { min = min, max = max };
end

---- Size ----
function chart:set_size(width, height)
	self.width, self.height = width, height;
end

function writers:size()
	return "chs="..tostring(self.width).."x"..tostring(self.height);
end

---- Title ----
function chart:set_title(title)
	self.title = title;
end

function writers:title()
	if self.title then
		return "chtt="..urlencode(tostring(self.title):gsub("\n", "|"));
	end
end

---- Legend ----
function chart:set_legend(entries)
	self.legend = entries;
end

function writers:legend()
	if self.legend then
		return "chdl="..table.concat(self.legend, "|");
	end
end

---- Legend position ----
local position_map = { 
			vertical = { bottom = "b", top = "t", left = "l", right = "r" };
			horizontal = { bottom = "b", top = "t" };
		};
		
function chart:set_legend_position(position, layout)
	self.legend_position = positionmap[layout or "vertical"][position or "right"] or position;
end

function writers:legend_position()
	if self.legend_position then
		return "chdlp="..self.legend_position;
	end
end
---- Axes display ----
local axismap = { bottom = "x", left = "y", top = "t", right = "r" };
function chart:add_axis(which, options)
	table.insert(self.axes, { type = axismap[which], options = options });
end

function writers:axes()
	local axes, ranges = {}, {};
	local labels, positions = {}, {};
	local styles, ticklengths = {}, {};
	
	for index, axis in ipairs(self.axes) do
		index = index - 1;
		table.insert(axes, axis.type);
		if axis.options.range then
			local range = axis.options.range;
			table.insert(ranges, index..","..(range.min or 0)..","..(range.max or 100)..(range.interval and (","..range.interval) or ""));
		end
		if axis.options.labels then
			if axis.options.labels[1] then -- A list of strings
				table.insert(labels, index..":|"..table.concat(axis.options.labels, "|"));
			else -- Specifying positions too
				local label_list, position_list = {}, {};
				for label, position in pairs(axis.options.labels) do
					table.insert(label_list, label);
					table.insert(position_list, position);
				end
				table.insert(labels, index..":|"..table.concat(label_list, "|"));
				table.insert(positions, index..","..table.concat(positions, ","));
			end
		end
		if axis.options.style then
			table.insert(styles, index..","..axis.options.style);
		end
		if axis.options.ticklength then
			table.insert(ticklengths, index..","..axis.options.ticklength);
		end
	end
	
	local result = {};
	
	if next(axes) then
		table.insert(result, "chxt="..urlencode(table.concat(axes, ",")));
	end
	if next(ranges) then
		table.insert(result, "chxr="..urlencode(table.concat(ranges, ",")));
	end
	if next(labels) then
		table.insert(result, "chxl="..urlencode(table.concat(labels, "|")));
	end
	if next(positions) then
		table.insert(result, "chxp="..urlencode(table.concat(positions, ",")));
	end
	if next(styles) then
		table.insert(result, "chxs="..urlencode(table.concat(styles, "|")));
	end
	if next(ticklengths) then
		table.insert(result, "chxtc="..urlencode(table.concat(ticklengths, "|")));
	end
	
	return table.concat(result, "&");
end

---- Data points ----
function chart:add_marker(marker)
	table.insert(self.markers, marker);
end

local marker_type_map = { flag = "f", text = "t", number = "N" };
function writers:markers()
	local result = { };
	for _, marker in ipairs(self.markers) do
		table.insert(result, urlencode(
			(marker_type_map[marker.type] or "f")
			..(marker.label or "Label")..","
			..(marker.color or self.marker_color)..","
			..(marker.series or 0)..","
			..(marker.index or 0)..","
			..(marker.size or 11)..","
			..(marker.priority or 0)));
	end
	if next(result) then
		return "chm="..table.concat(result, "%7c");
	end
end

---- Colours and fill ----
function chart:set_color(color)
	self.color = color;
end

function chart:set_fill(fill_color)
	self.fill = fill_color;
end

function writers:color()
	if self.color then
		return "chco="..self.color;
	end
end

function chart:url()
	local url = self.base_url.."?cht="..self.type.."&";
	
	local params = {};
	for name, writer in pairs(writers) do
		local ret = writer(self);
		if ret then
			table.insert(params, tostring(ret));
		end
	end

	return url..table.concat(params, "&");
end

mercurial