Login   Register  
PHP Classes
elePHPant
Icontem

File: SVGraph.js

Recommend this page to a friend!
Stumble It! Stumble It! Bookmark in del.icio.us Bookmark in del.icio.us
  Classes of Victor Bolshov  >  SVGraph  >  SVGraph.js  >  Download  
File: SVGraph.js
Role: Auxiliary data
Content type: text/plain
Description: SVGraph class - javascript analogue
Class: SVGraph
Generate bar charts in SVG format
Author: By
Last change: Added method setPaddings() for greater customization, refactored the code.
Date: 7 years ago
Size: 9,687 bytes
 

Contents

Class file image Download
/**
 * SVGraph is a JS class for creating SVG-diagrams displaying numeric data
 */

/** Constructor
 * @param int width
 * @param int height
 */
SVGraph = function(width, height) {
	this.outerWidth = width
	this.outerHeight = height
	this.innerWidth = null
	this.innerHeight = null
	this.paddingLeft = null
	this.paddingBottom = null
	this.paddingTop = null
	this.paddingRight = null
	this.x = []
	this.y = []
	this.legends = {}
	this.max = null
	this.sf = null
	this.factors = {}
	// if you have to render more than 10 lines, use setColor() to add your own colors.
	// You may as well overwrite default colors with setColor()
	this.colors = ['red', 'green', 'blue', 'orange', 'magenta', 'darkblue', 'maroon', 'indigo', 'peru', 'teal']
	this.svg = false
	this.rendered = false
	this.setDefaultPaddings()
}
SVGraph.prototype = {
	/**
	 * Import user data
	 * @param Array[] - an array of data-records, each record being itself an array like this: [x, y1, y2,.. ,yN]
	 */
	load: function(data) {
		// reset data.
		// NOTE: current setup (factors, legends, colors is NOT reset!!!)
		this.y = []
		this.x = []
		this.max = null
		this.min = null// @todo: use this for zero-point to make layout more flexible
		this.sf = null
		for (var i = 0; i < data.length; ++i)
		{
			this.x[i] = data[i][0]// X-axis
			for (var j = 1; j < data[i].length; ++j)
			{
				// Y-axis - for all lines we have to render
				var v = parseFloat(data[i][j])
				if ((j - 1) in this.factors)
				{
					v = v / this.factors[j - 1]
				}
				if ((this.max == null) || (v > this.max))
				{
					this.max = v
					this.sf = v / this.innerHeight
				}
				if (this.y.length < j)
				{
					this.y[j - 1] = []
				}
				this.y[j - 1][i] = v
			}
		}
		return this
	},
	/**
	 * Set paddings.
	 * 
	 * Each value may come in the form of
	 * - integer: number of pixels
	 * - float: a part of outer size
	 * - string (N%): a percentage of outer size
	 *
	 * @param int | float | string $top
	 * @param int | float | string $right
	 * @param int | float | string $bottom
	 * @param int | float | string $left
	 */
	setPaddings: function(top, right, bottom, left)
	{
		top = this.absolutePadding(top, this.outerHeight);
		right = this.absolutePadding(right, this.outerWidth);
		bottom = this.absolutePadding(bottom, this.outerHeight);
		left = this.absolutePadding(left, this.outerWidth);
		this.paddingLeft = left;
		this.paddingBottom = bottom;
		this.paddingRight = right;
		this.paddingTop = top;
		this.innerWidth = this.outerWidth - left - right;
		var oldInnerHeight = this.innerHeight;
		this.innerHeight = this.outerHeight - top - bottom;
		// update scale factor
		if ((oldInnerHeight != this.innerHeight) && null !== this.sf)
		{
			this.sf = this.sf * (this.innerHeight / oldInnerHeight);
		}
	},
	absolutePadding: function(value, outerSize)
	{
		if ('number' == typeof(value))
		{
			if (value < 1)
			{
				return value * outerSize;
			} else {
				return value
			}
		} else {
			return (parseInt(value) * outerSize) / 100;
		}
	},
	setDefaultPaddings: function()
	{
		this.setPaddings(.1, .1, .25, .15);
	},
	// @access private
	sy: function(v) {
		return this.outerHeight - (this.paddingBottom + (v / this.sf));
	},
	sx: function(x)
	{
		return this.paddingLeft + x;
	},
	/**
	 * Set legend for line #n
	 * @param int n
	 * @param string legend
	 */
	setLegend: function(n, legend) {
		this.legends[n] = legend
		return this
	},
	/**
	 * Set factor for line #n - so that every data value is devided by this factor before being rendered
	 * @param int n
	 * @param numeric factor
	 */
	setFactor: function(n, factor) {
		this.factors[n] = factor
		return this
	},
	/**
	 * Set color for line #n
	 * @param int n
	 * @param string color
	 */
	setColor: function(n, color) {
		this.colors[n] = color
		return this
	},
	/**
	 * Once SVGraph is rendered, it marks itself as "rendered".
	 * With renderOnce() method you can always be sure that your SVGraph instance
	 * only renders itself once.
	 */
	renderOnce: function(container) {
		if (this.rendered) return;
		this.render(container)
	},
	/**
	 * Render graphic inside container element
	 * @param string container the container element #ID
	 */
	render: function(container) {
		if (1 >= this.x.length)
		{// nothing to render
			return
		}
		container = document.getElementById(container)
		
		// root SVG element
		this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
		this.svg.setAttribute('width', this.outerWidth);
		this.svg.setAttribute('height', this.outerHeight);
		
		var xstep = Math.round(this.innerWidth / (this.x.length - 1))
		
		this.renderGrid(xstep)
		
		// main cycle
		var x1, x2, y1, y2, c
		for (var j = 0; j < this.y.length; ++j)
		{
			c = this.getColor(j)
			// wrap every line-set in a group element,
			// so that we are able to menipulate with it via scritp later
			var group = document.createElementNS('http://www.w3.org/2000/svg', 'g')
			group.setAttribute('id', 'g' + j)
			var title = group.appendChild(document.createElementNS('http://www.w3.org/2000/svg', 'title'))
			var titleText = this.getLegend(j)
			title.appendChild(document.createTextNode(titleText))
			group.setAttribute('title', titleText)// FF doesn't show <title> otherwise :(
			for (var i = 0; i < this.x.length; ++i) {
				if (i) {
					// create a line part
					x1 = (i - 1) * xstep;
					x2 = i * xstep;
					y1 = this.y[j][i - 1];
					y2 = this.y[j][i]
					var line = this.line(group, x1, y1, x2, y2)
					this.style(line, "stroke:" + c + ";stroke-width:2;")
					
					// create data point
					var point = group.appendChild(document.createElementNS('http://www.w3.org/2000/svg', 'circle'))
					this.style(point, "stroke:none;fill:" + c + ";cursor:pointer;")
					point.setAttribute('cx', this.sx(x2))
					point.setAttribute('cy', this.sy(y2))
					point.setAttribute('r', 5)
					
					// data point <title>
					var displayedValue = this.y[j][i]
					if (j in this.factors)
					{
						displayedValue = displayedValue * this.factors[j]
					}
					var text = this.getLegend(j, false) + " = " + displayedValue
					var pointTitle = point.appendChild(document.createElementNS('http://www.w3.org/2000/svg', 'title'))
					pointTitle.appendChild(document.createTextNode(text))
					point.setAttribute('title', text)
				}
			}
			this.style(group, "display:block;")
			this.svg.appendChild(group)
		}
		container.appendChild(this.svg)
		this.renderLegends(container)
		this.rendered = true
	},
	sx: function(x) {
		return this.paddingLeft + x;
	},
	// @access private
	sy: function(v) {
		return this.outerHeight - (this.paddingBottom + (v / this.sf));
	},
	// @access private
	line: function(parent, x1, y1, x2, y2) {
		var line = document.createElementNS('http://www.w3.org/2000/svg', 'line')
		line.setAttribute('x1', this.sx(x1))
		line.setAttribute('y1', this.sy(y1))
		line.setAttribute('x2', this.sx(x2))
		line.setAttribute('y2', this.sy(y2))
		return parent.appendChild(line)
	},
	// @access private
	style: function(el, s) {
		el.setAttribute('style', s)
	},
	// @access private
	getLegend: function(index, respectFactor) {
		if (arguments.length < 2) respectFactor = true
		var l = (index in this.legends) ? this.legends[index] : 'Curve #' + j
		if (respectFactor && (index in this.factors))
		{
			l += " (x" + this.factors[index] + ")"
		}
		return l
	},
	// @access private
	getColor: function(index) {
		return (index < this.colors.length) ? this.colors[index] : 'black'
	},
	// @access private
	renderGrid: function(xstep) {
		// X-Y axis
		var x0 = this.line(this.svg, 0, 0, 0, this.innerHeight * this.sf)
		this.style(x0, 'stroke:black;')
		var y0 = this.line(this.svg, 0, 0, this.innerWidth, 0)
		this.style(y0, 'stroke:black;')
		
		// vertical grid
		for (var i = 0; i < this.x.length; ++i)
		{
			var text = document.createElementNS('http://www.w3.org/2000/svg', 'text')
			text.appendChild(document.createTextNode(this.x[i]))
			
			var x = i * xstep
			var tx = this.sx(x)
			var ty = this.outerHeight - this.paddingBottom + 10
			text.setAttribute('x', tx)
			text.setAttribute('y', ty)
			text.setAttribute('transform', 'rotate(45,' + tx + ',' + ty + ')')
			this.style(text, "font-size:6;")
			this.svg.appendChild(text)
			
			var xmarker = this.line(this.svg, x, 0, x, this.innerHeight * this.sf)
			this.style(xmarker, 'stroke:black;stroke-width:.2;')
		}
		
		// horizontal grid
		var ystep = Math.pow(10, Math.floor(Math.log(this.max) / Math.log(10)))
		for (var j = ystep; j <= this.max; j += ystep)
		{
			var text = document.createElementNS('http://www.w3.org/2000/svg', 'text')
			text.appendChild(document.createTextNode(j))
			text.setAttribute('x', '1')
			text.setAttribute('y', this.sy(j))
			this.svg.appendChild(text)
			
			var line = this.line(this.svg, 0, j, this.innerWidth, j)
			this.style(line, 'stroke:black;stroke-width:.2;')
		}
	},
	// @access private
	renderLegends: function(container) {
		var div = container.appendChild(document.createElement('div'))
		for (var j = 0; j < this.y.length; ++j)
		{
			// @todo: style some properies of the legends via CSS-classes, not with embedded style defs
			var a = document.createElement('a')
			a.setAttribute('href', '')
			a.setAttribute('onclick', 'SVGraph.toggle("g' + j + '"); return false;')
			a.style.marginRight = '1em'
			a.style.padding = '.2em .5em'
			a.style.cursor = 'pointer'
			a.style.backgroundColor = this.getColor(j)
			a.style.color = 'white'
			a.innerHTML = this.getLegend(j)
			div.appendChild(a)
		}
	}
}
// toggle a line
SVGraph.toggle = function(id) {
	var el = document.getElementById(id)
	if ('none' == el.style.display)
	{
		el.style.display = 'block'
	} else {
		el.style.display = 'none'
	}
}