/* PlotKit SVG =========== SVG Renderer for PlotKit Copyright --------- Copyright 2005,2006 (c) Alastair Tse For use under the BSD license. */ // ------------------------------------------------------------------------- // NOTES: - If you use XHTML1.1 strict, then you must include each MochiKit // file individuall. // - For IE support, you must include the AdobeSVG object hack. // See tests/svg.html for details. // ------------------------------------------------------------------------- // ------------------------------------------------------------------------- // Check required components // ------------------------------------------------------------------------- try { if (typeof(PlotKit.Layout) == 'undefined') { throw ""; } } catch (e) { throw "PlotKit depends on MochiKit.{Base,Color,DOM,Format} and PlotKit.Layout" } // --------------------------------------------------------------------------- // SVG Renderer // --------------------------------------------------------------------------- PlotKit.SVGRenderer = function(element, layout, options) { if (arguments.length > 0) this.__init__(element, layout, options); }; PlotKit.SVGRenderer.NAME = "PlotKit.SVGRenderer"; PlotKit.SVGRenderer.VERSION = PlotKit.VERSION; PlotKit.SVGRenderer.__repr__ = function() { return "[" + this.NAME + " " + this.VERSION + "]"; }; PlotKit.SVGRenderer.toString = function() { return this.__repr__(); } PlotKit.SVGRenderer.isSupported = function() { // TODO return true; }; PlotKit.SVGRenderer.prototype.__init__ = function(element, layout, options) { var isNil = MochiKit.Base.isUndefinedOrNull; // default options this.options = { "drawBackground": true, "backgroundColor": Color.whiteColor(), "padding": {left: 30, right: 30, top: 5, bottom: 10}, "colorScheme": PlotKit.Base.palette(PlotKit.Base.baseColors()[1]), "strokeColor": Color.whiteColor(), "strokeColorTransform": "asStrokeColor", "strokeWidth": 0.5, "shouldFill": true, "shouldStroke": true, "drawXAxis": true, "drawYAxis": true, "axisLineColor": Color.blackColor(), "axisLineWidth": 0.5, "axisTickSize": 3, "axisLabelColor": Color.blackColor(), "axisLabelFont": "Arial", "axisLabelFontSize": 9, "axisLabelWidth": 50, "axisLabelUseDiv": true, "pieRadius": 0.4, "enableEvents": true }; MochiKit.Base.update(this.options, options ? options : {}); this.layout = layout; this.style = layout.style; this.element = MochiKit.DOM.getElement(element); this.container = this.element.parentNode; this.height = parseInt(this.element.getAttribute("height")); this.width = parseInt(this.element.getAttribute("width")); this.document = document; this.root = this.element; // Adobe SVG Support: // - if an exception is thrown, then no Adobe SVG Plugin support. try { this.document = this.element.getSVGDocument(); this.root = isNil(this.document.documentElement) ? this.element : this.document.documentElement; } catch (e) { } this.element.style.zIndex = 1; if (isNil(this.element)) throw "SVGRenderer() - passed SVG object is not found"; if (isNil(this.container) || this.container.nodeName.toLowerCase() != "div") throw "SVGRenderer() - No DIV's around the SVG."; // internal state this.xlabels = new Array(); this.ylabels = new Array(); // initialise some meta structures in SVG this.defs = this.createSVGElement("defs"); this.area = { x: this.options.padding.left, y: this.options.padding.top, w: this.width - this.options.padding.left - this.options.padding.right, h: this.height - this.options.padding.top - this.options.padding.bottom }; MochiKit.DOM.updateNodeAttributes(this.container, {"style":{ "position": "relative", "width": this.width + "px"}}); }; PlotKit.SVGRenderer.prototype.render = function() { if (this.options.drawBackground) this._renderBackground(); if (this.style == "bar") { this._renderBarChart(); this._renderBarAxis(); } else if (this.style == "pie") { this._renderPieChart(); this._renderPieAxis(); } else if (this.style == "line") { this._renderLineChart(); this._renderLineAxis(); } }; PlotKit.SVGRenderer.prototype._renderBarOrLine = function(data, plotFunc, startFunc, endFunc) { var colorCount = this.options.colorScheme.length; var colorScheme = this.options.colorScheme; var setNames = MochiKit.Base.keys(this.layout.datasets); var setCount = setNames.length; for (var i = 0; i < setCount; i++) { var setName = setNames[i]; var attrs = new Array(); var color = colorScheme[i%colorCount]; if (this.options.shouldFill) attrs["fill"] = color.toRGBString(); else attrs["fill"] = "none"; if (this.options.shouldStroke && (this.options.strokeColor || this.options.strokeColorTransform)) { if (this.options.strokeColor) attrs["stroke"] = this.options.strokeColor.toRGBString(); else if (this.options.strokeColorTransform) attrs["stroke"] = color[this.options.strokeColorTransform]().toRGBString(); attrs["strokeWidth"] = this.options.strokeWidth; } if (startFunc) startFunc(attrs); var forEachFunc = function(obj) { if (obj.name == setName) plotFunc(attrs, obj); }; MochiKit.Iter.forEach(data, bind(forEachFunc, this)); if (endFunc) endFunc(attrs); } }; PlotKit.SVGRenderer.prototype._renderBarChart = function() { var bind = MochiKit.Base.bind; var drawRect = function(attrs, bar) { var x = this.area.w * bar.x + this.area.x; var y = this.area.h * bar.y + this.area.y; var w = this.area.w * bar.w; var h = this.area.h * bar.h; this._drawRect(x, y, w, h, attrs); }; this._renderBarOrLine(this.layout.bars, bind(drawRect, this)); }; PlotKit.SVGRenderer.prototype._renderLineChart = function() { var bind = MochiKit.Base.bind; var addPoint = function(attrs, point) { this._tempPointsBuffer += (this.area.w * point.x + this.area.x) + "," + (this.area.h * point.y + this.area.y) + " "; }; var startLine = function(attrs) { this._tempPointsBuffer = ""; this._tempPointsBuffer += (this.area.x) + "," + (this.area.y+this.area.h) + " "; }; var endLine = function(attrs) { this._tempPointsBuffer += (this.area.w + this.area.x) + "," +(this.area.h + this.area.y); attrs["points"] = this._tempPointsBuffer; var elem = this.createSVGElement("polygon", attrs); this.root.appendChild(elem); }; this._renderBarOrLine(this.layout.points, bind(addPoint, this), bind(startLine, this), bind(endLine, this)); }; PlotKit.SVGRenderer.prototype._renderPieChart = function() { var colorCount = this.options.colorScheme.length; var slices = this.layout.slices; var centerx = this.area.x + this.area.w * 0.5; var centery = this.area.y + this.area.h * 0.5; var radius = Math.min(this.area.w * this.options.pieRadius, this.area.h * this.options.pieRadius); // NOTE NOTE!! Canvas Tag draws the circle clockwise from the y = 0, x = 1 // so we have to subtract 90 degrees to make it start at y = 1, x = 0 // workaround if we only have 1 slice of 100% if (slices.length == 1 && (Math.abs(slices[0].startAngle) - Math.abs(slices[0].endAngle) < 0.1)) { var attrs = {"cx": centerx , "cy": centery , "r": radius }; var color = this.options.colorScheme[0]; if (this.options.shouldFill) attrs["fill"] = color.toRGBString(); else attrs["fill"] = "none"; if (this.options.shouldStroke && (this.options.strokeColor || this.options.strokeColorTransform)) { if (this.options.strokeColor) attrs["stroke"] = this.options.strokeColor.toRGBString(); else if (this.options.strokeColorTransform) attrs["stroke"] = color[this.options.strokeColorTransform]().toRGBString(); attrs["style"] = "stroke-width: " + this.options.strokeWidth; } this.root.appendChild(this.createSVGElement("circle", attrs)); return; } for (var i = 0; i < slices.length; i++) { var attrs = new Array(); var color = this.options.colorScheme[i%colorCount]; if (this.options.shouldFill) attrs["fill"] = color.toRGBString(); else attrs["fill"] = "none"; if (this.options.shouldStroke && (this.options.strokeColor || this.options.strokeColorTransform)) { if (this.options.strokeColor) attrs["stroke"] = this.options.strokeColor.toRGBString(); else if (this.options.strokeColorTransform) attrs["stroke"] = color[this.options.strokeColorTransform]().toRGBString(); attrs["style"] = "stroke-width:" + this.options.strokeWidth; } var largearc = 0; if (Math.abs(slices[i].endAngle - slices[i].startAngle) > Math.PI) largearc = 1; var x1 = Math.cos(slices[i].startAngle - Math.PI/2) * radius; var y1 = Math.sin(slices[i].startAngle - Math.PI/2) * radius; var x2 = Math.cos(slices[i].endAngle - Math.PI/2) * radius; var y2 = Math.sin(slices[i].endAngle - Math.PI/2) * radius; var rx = x2 - x1; var ry = y2 - y1; var pathString = "M" + centerx + "," + centery + " "; pathString += "l" + x1 + "," + y1 + " "; pathString += "a" + radius + "," + radius + " 0 " + largearc + ",1 " + rx + "," + ry + " z"; attrs["d"] = pathString; var elem = this.createSVGElement("path", attrs); this.root.appendChild(elem); } }; PlotKit.SVGRenderer.prototype._renderBarAxis = function() { this._renderAxis(); } PlotKit.SVGRenderer.prototype._renderLineAxis = function() { this._renderAxis(); }; PlotKit.SVGRenderer.prototype._renderAxis = function() { if (!this.options.drawXAxis && !this.options.drawYAxis) return; var labelStyle = {"style": {"position": "absolute", "textAlign": "center", "fontSize": this.options.axisLabelFontSize + "px", "zIndex": 10, "color": this.options.axisLabelColor.toRGBString(), "width": this.options.axisLabelWidth + "px", "overflow": "hidden" } }; // axis lines var lineAttrs = { "stroke": this.options.axisLineColor.toRGBString(), "strokeWidth": this.options.axisLineWidth }; if (this.options.drawYAxis) { if (this.layout.yticks) { var drawTick = function(tick) { var x = this.area.x; var y = this.area.y + tick[0] * this.area.h; this._drawLine(x, y, x - 3, y, lineAttrs); if (this.options.axisLabelUseDiv) { var label = DIV(labelStyle, tick[1]); label.style.top = (y - this.options.axisLabelFontSize) + "px"; label.style.left = (x - this.options.padding.left + this.options.axisTickSize) + "px"; label.style.textAlign = "left"; label.style.width = (this.options.padding.left - 3) + "px"; MochiKit.DOM.appendChildNodes(this.container, label); this.ylabels.push(label); } else { var attrs = { y: y + 3, x: (x - this.options.padding.left + 3), width: (this.options.padding.left - this.options.axisTickSize) + "px", height: (this.options.axisLabelFontSize + 3) + "px", fontFamily: "Arial", fontSize: this.options.axisLabelFontSize + "px", fill: this.options.axisLabelColor.toRGBString() }; /* we can do clipping just like DIVs http://www.xml.com/pub/a/2004/06/02/svgtype.html */ /* var mask = this.createSVGElement("mask", {id: "mask" + tick[0]}); var maskShape = this.createSVGElement("rect", {y: y + 3, x: (x - this.options.padding.left + 3), width: (this.options.padding.left - this.options.axisTickSize) + "px", height: (this.options.axisLabelFontSize + 3) + "px", style: {"fill": "#ffffff", "stroke": "#000000"}}); mask.appendChild(maskShape); this.defs.appendChild(mask); attrs["filter"] = "url(#mask" + tick[0] + ")"; */ var label = this.createSVGElement("text", attrs); label.appendChild(this.document.createTextNode(tick[1])); this.root.appendChild(label); } }; MochiKit.Iter.forEach(this.layout.yticks, bind(drawTick, this)); } this._drawLine(this.area.x, this.area.y, this.area.x, this.area.y + this.area.h, lineAttrs); } if (this.options.drawXAxis) { if (this.layout.xticks) { var drawTick = function(tick) { var x = this.area.x + tick[0] * this.area.w; var y = this.area.y + this.area.h; this._drawLine(x, y, x, y + this.options.axisTickSize, lineAttrs); if (this.options.axisLabelUseDiv) { var label = DIV(labelStyle, tick[1]); label.style.top = (y + this.options.axisTickSize) + "px"; label.style.left = (x - this.options.axisLabelWidth/2) + "px"; label.style.textAlign = "center"; label.style.width = this.options.axisLabelWidth + "px"; MochiKit.DOM.appendChildNodes(this.container, label); this.xlabels.push(label); } else { var attrs = { y: (y + this.options.axisTickSize + this.options.axisLabelFontSize), x: x - 3, width: this.options.axisLabelWidth + "px", height: (this.options.axisLabelFontSize + 3) + "px", fontFamily: "Arial", fontSize: this.options.axisLabelFontSize + "px", fill: this.options.axisLabelColor.toRGBString(), textAnchor: "middle" }; var label = this.createSVGElement("text", attrs); label.appendChild(this.document.createTextNode(tick[1])); this.root.appendChild(label); } }; MochiKit.Iter.forEach(this.layout.xticks, bind(drawTick, this)); } this._drawLine(this.area.x, this.area.y + this.area.h, this.area.x + this.area.w, this.area.y + this.area.h, lineAttrs) } }; PlotKit.SVGRenderer.prototype._renderPieAxis = function() { if (this.layout.xticks) { // make a lookup dict for x->slice values var lookup = new Array(); for (var i = 0; i < this.layout.slices.length; i++) { lookup[this.layout.slices[i].xval] = this.layout.slices[i]; } var centerx = this.area.x + this.area.w * 0.5; var centery = this.area.y + this.area.h * 0.5; var radius = Math.min(this.area.w * this.options.pieRadius + 10, this.area.h * this.options.pieRadius + 10); var labelWidth = this.options.axisLabelWidth; for (var i = 0; i < this.layout.xticks.length; i++) { var slice = lookup[this.layout.xticks[i][0]]; if (MochiKit.Base.isUndefinedOrNull(slice)) continue; var angle = (slice.startAngle + slice.endAngle)/2; // normalize the angle var normalisedAngle = angle; if (normalisedAngle > Math.PI * 2) normalisedAngle = normalisedAngle - Math.PI * 2; else if (normalisedAngle < 0) normalisedAngle = normalisedAngle + Math.PI * 2; var labelx = centerx + Math.sin(normalisedAngle) * (radius + 10); var labely = centery - Math.cos(normalisedAngle) * (radius + 10); var attrib = { "position": "absolute", "zIndex": 11, "width": labelWidth + "px", "fontSize": this.options.axisLabelFontSize + "px", "overflow": "hidden", "color": this.options.axisLabelColor.toHexString() }; var svgattrib = { "width": labelWidth + "px", "fontSize": this.options.axisLabelFontSize + "px", "height": (this.options.axisLabelFontSize + 3) + "px", "fill": this.options.axisLabelColor.toRGBString() }; if (normalisedAngle <= Math.PI * 0.5) { // text on top and align left MochiKit.Base.update(attrib, { 'textAlign': 'left', 'verticalAlign': 'top', 'left': labelx + 'px', 'top': (labely - this.options.axisLabelFontSize) + "px" }); MochiKit.Base.update(svgattrib, { "x": labelx, "y" :(labely - this.options.axisLabelFontSize), "textAnchor": "left" }); } else if ((normalisedAngle > Math.PI * 0.5) && (normalisedAngle <= Math.PI)) { // text on bottom and align left MochiKit.Base.update(attrib, { 'textAlign': 'left', 'verticalAlign': 'bottom', 'left': labelx + 'px', 'top': labely + "px" }); MochiKit.Base.update(svgattrib, { 'textAnchor': 'left', 'x': labelx, 'y': labely }); } else if ((normalisedAngle > Math.PI) && (normalisedAngle <= Math.PI*1.5)) { // text on bottom and align right MochiKit.Base.update(attrib, { 'textAlign': 'right', 'verticalAlign': 'bottom', 'left': labelx + 'px', 'top': labely + "px" }); MochiKit.Base.update(svgattrib, { 'textAnchor': 'right', 'x': labelx - labelWidth, 'y': labely }); } else { // text on top and align right MochiKit.Base.update(attrib, { 'textAlign': 'left', 'verticalAlign': 'bottom', 'left': labelx + 'px', 'top': labely + "px" }); MochiKit.Base.update(svgattrib, { 'textAnchor': 'left', 'x': labelx - labelWidth, 'y': labely - this.options.axisLabelFontSize }); } if (this.options.axisLabelUseDiv) { var label = DIV({'style': attrib}, this.layout.xticks[i][1]); this.xlabels.push(label); MochiKit.DOM.appendChildNodes(this.container, label); } else { var label = this.createSVGElement("text", svgattrib); label.appendChild(this.document.createTextNode(this.layout.xticks[i][1])) this.root.appendChild(label); } } } }; PlotKit.SVGRenderer.prototype._renderBackground = function() { var opts = {"stroke": "none", "fill": this.options.backgroundColor.toRGBString() }; this._drawRect(0, 0, this.width, this.height, opts); }; PlotKit.SVGRenderer.prototype._drawRect = function(x, y, w, h, moreattrs) { var attrs = {x: x + "px", y: y + "px", width: w + "px", height: h + "px"}; if (moreattrs) MochiKit.Base.update(attrs, moreattrs); var elem = this.createSVGElement("rect", attrs); this.root.appendChild(elem); }; PlotKit.SVGRenderer.prototype._drawLine = function(x1, y1, x2, y2, moreattrs) { var attrs = {x1: x1 + "px", y1: y1 + "px", x2: x2 + "px", y2: y2 + "px"}; if (moreattrs) MochiKit.Base.update(attrs, moreattrs); var elem = this.createSVGElement("line", attrs); this.root.appendChild(elem); } PlotKit.SVGRenderer.prototype.clear = function() { while(this.element.firstChild) { this.element.removeChild(this.element.firstChild); } if (this.options.axisLabelUseDiv) { for (var i = 0; i < this.xlabels.length; i++) { MochiKit.DOM.removeElement(this.xlabels[i]); } for (var i = 0; i < this.ylabels.length; i++) { MochiKit.DOM.removeElement(this.ylabels[i]); } } this.xlabels = new Array(); this.ylabels = new Array(); }; PlotKit.SVGRenderer.prototype.createSVGElement = function(name, attrs) { var isNil = MochiKit.Base.isUndefinedOrNull; var elem; var doc = isNil(this.document) ? document : this.document; try { elem = doc.createElementNS("http://www.w3.org/2000/svg", name); } catch (e) { elem = doc.createElement(name); elem.setAttribute("xmlns", "http://www.w3.org/2000/svg"); } if (attrs) MochiKit.DOM.updateNodeAttributes(elem, attrs); // TODO: we don't completely emulate the MochiKit.DOM.createElement // as we don't care about nodes contained. We really should though. return elem; }; PlotKit.SVGRenderer.SVGNS = 'http://www.w3.org/2000/svg'; PlotKit.SVGRenderer.SVG = function(attrs) { // we have to do things differently for IE+AdobeSVG. // My guess this works (via trial and error) is that we need to // have an SVG object in order to use SVGDocument.createElementNS // but IE doesn't allow us to that. var ie = navigator.appVersion.match(/MSIE (\d\.\d)/); var opera = (navigator.userAgent.toLowerCase().indexOf("opera") != -1); if (ie && (ie[1] >= 6) && (!opera)) { var width = attrs["width"] ? attrs["width"] : "100"; var height = attrs["height"] ? attrs["height"] : "100"; var eid = attrs["id"] ? attrs["id"] : "notunique"; var html = ''; var canvas = document.createElement(html); // create embedded SVG inside SVG. var group = canvas.getSVGDocument().createElementNS(PlotKit.SVGRenderer.SVGNS, "svg"); group.setAttribute("width", width); group.setAttribute("height", height); canvas.getSVGDocument().appendChild(group); return canvas; } else { return PlotKit.SVGRenderer.prototype.createSVGElement("svg", attrs); } }; PlotKit.SVGRenderer.isSupported = function() { var isOpera = (navigator.userAgent.toLowerCase().indexOf("opera") != -1); var ieVersion = navigator.appVersion.match(/MSIE (\d\.\d)/); var safariVersion = navigator.userAgent.match(/AppleWebKit\/(\d+)/); var operaVersion = navigator.userAgent.match(/Opera\/(\d*\.\d*)/); var mozillaVersion = navigator.userAgent.match(/rv:(\d*\.\d*).*Gecko/); if (ieVersion && (ieVersion[1] >= 6) && !isOpera) { var dummysvg = document.createElement(''); try { dummysvg.getSVGDocument(); dummysvg = null; return true; } catch (e) { return false; } } /* support not really there yet. no text and paths are buggy if (safariVersion && (safariVersion[1] > 419)) return true; */ if (operaVersion && (operaVersion[1] > 8.9)) return true if (mozillaVersion && (mozillaVersion > 1.7)) return true; return false; };