/**
* 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'
}
}
|