Tuesday, July 14, 2009

AJAX and PHP Building Responsive Web Applications by Cristian Darie, Bogdan Brinzarea Chapter 7

AJAX Real-Time Charting with
SVG

Scalable Vector Graphics (SVG) is one of the emerging technologies with a good chance of becoming the next "big thing" for web graphics, as was the case with Flash. SVG is a language for defining two-dimensional graphics. SVG isn't necessarily related to web development, but it fits very well into the picture because it complements the features offered naturally by web browsers. Today, there are more SVG implementations and standardization isn't great, but things are expected to improve in the future.

SVG is a World Wide Web Consortium (W3C) recommendation since January 2003. Among the big names that have contributed to its creation we can mention Sun Microsystems, Adobe, Apple, IBM, and Kodak to name just a few. The current specification is SVG 1.2. SVG Mobile, SVG Print, and sXBL are other recommendations under work at W3C that are likely to get support on most browsers and platforms.

The main features of SVG are:

• SVG graphics are defined in XML format, so the files can be easily manipulated with existing editors, parsers, etc.
• SVG images are scalable; they can zoomed, resized, and reoriented without losing quality.
• SVG includes font elements so that both text and graphical appearance are preserved.
• SVG includes declarative animation elements.
• SVG allows a multi-namespace XML application.
• SVG allows the script-based manipulation of the document tree using a subset of the
XML DOM and the SVG uDOM.

For a primer on the world of SVG, check out these resources:

• The SVG W3C page at http://www.w3.org/Graphics/SVG/.
• An SVG introduction at http://www.w3schools.com/svg/svg_intro.asp.
• A very useful list of SVG links at http://www.svgi.org/.
• A handy SVG reference at http://www.w3schools.com/svg/svg_reference.asp.
• The SVG document structure is explained at http://www.w3.org/TR/SVG/struct.html.
• SVG examples at http://www.carto.net/papers/svg/samples/ and http://svg- whiz.com/samples.html.

Implementing a Real-Time Chart with AJAX and SVG Before continuing, please make sure your web browser supports SVG. The code in this case study has been tested with Firefox 1.5, Internet Explorer with the Adobe SVG Viewer, and Apache Batik.
You can test the online demo accessing the link you can find at http://ajaxphp.packtpub.com.

Firefox ships with integrated SVG support. Being at its first version, this SVG implementation does have some problems that you need to take into consideration when writing the code, and the performance isn't excellent.

To load SVG in Internet Explorer, you need to install an external SVG plug-in. The SVG plug-in we used in our tests is the one from Adobe, which you can download at http://www.adobe.com/ svg/viewer/install/main.html. The installation process is very simple; you just need to download a small file named SVGView.exe, and execute it. The first time you load an SVG page, you will be asked to confirm the terms of agreement.

Finally, we also tested the application with Apache's Batik SVG viewer, in which case you need to load the SVG file directly, because it doesn't support loading the HTML file that loads the SVG script. (You may want to check Batik for its good DOM viewer, which nicely displays the SVG nodes in a hierarchical structure.)

In this chapter's case study, we'll create a simple chart application whose input data is retrieved asynchronously from a PHP script. The generated data can be anything, and in our case we'll have a simple algorithm that generates random data. Figure 7.1 shows sample output from the application:

Figure 7.1: SVG Chart

The chart in Figure 7.1 is actually a static SVG file called temp.svg, which represents a snapshot of the output generated by the running application; it is not a screenshot of the actual running application. The script is saved as temp.svg in this chapter's folder in the code download, and you can load it directly into your web browser (not necessarily through a web server), after you've made sure your browser supports SVG.

We will first have a look at the contents of temp.svg, to get a feeling about what we want to generate dynamically from our JavaScript code. Note that the SVG script can be generated either at the client side or at the server side. For our application, the server only generates random coordinates, and the JavaScript client uses these coordinates to compose the SVG output.

Have a look at a stripped version of the temp.svg file:

<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" onload="init(evt)">
<a xlink:href="http://ajaxphp.packtpub.com">
<text x="200" y="20">
SVG with AJAX and PHP Demo
</text>
</a>
<!-- All chart elements are grouped and translated by 50, 50 -->
<g transform="translate(50, 50)">

<!-- Group all axis elements (lines and text nodes) -->
<g>
<!-- Path draws the grid axes and unit delimiters -->
<path stroke="black" stroke-width="2" d=" ... path definition here ..."/>

<!-- Text nodes that display horizontal unit numbers -->
<text x="-10" y="322" stroke="black">0.0</text>
...
... more text nodes here that draw horizontal and vertical unit numbers
...
</g>

<!-- Draw the lines between chart nodes as a single -->
<path stroke="black" stroke-width="1" fill="none" d="... definition ..."/>

<!-- Draw the chart nodes as filled blue circles -->
<circle cx="00" cy="239.143" r="3" fill="blue" />
...
... more circle nodes here that draw filled blue circles for chart nodes
...
</g>
</svg>

Have a closer look at this code snippet to identify all the chart elements. The SVG format supports the notion of element groups, which are elements grouped under a <g> element. In temp.svg we have two groups: the first group contains all the charts' elements, translating them by (50, 50) pixels, while the second <g> element group is a child of the first group, and it contains the chart's axis lines and numbers.

SVG knows how to handle many element types, which can also be animated (yes, SVG is very powerful). In our example, we make use of some of the very basic ones: path (to draw the axis lines and chart lines), text (to draw the axis numbers, and to dynamically display chart node coordinates when the mouse cursor hovers over them—this latter feature isn't included in the code snippet), and circle (to draw the blue dots on the chart that represent the chart nodes).

You can find documentation for these elements at:

• http://www.w3schools.com/svg/svg_path.asp
• http://www.w3schools.com/svg/svg_circle.asp
• http://www.w3schools.com/svg/svg_text.asp

The paths are described by a path definition. The complete code for the path element that draws the chart lines you can see in Figure 7.1 looks like this:

<!-- Draw the lines between chart nodes -->
<path stroke="black" stroke-width="1" fill="none"
d="M0,239.143 L10,220.286 L20,213.429 L30,185.571 L40,145.714
L50,108.857 L60,129 L70,101.143 L80,58.2857 L90,78.4286"/>

A detail that was stripped from the code snippet was the mouseover and mouseout events of the chart node circles. In our code, the mouseover event (which fires when you move the mouse pointer over a node) will call a JavaScript function that displays a text above the node specifying its coordinates. The mouseout event makes that text disappear. You can see this feature in action in Figure 7.2, which displays the SVG chart application in action.

Figure 7.2: SVG Charting in Action

To get the dynamically generated contents of the SVG chart at any given time with Firefox, right click the chart, click Select All, then right-click the chart again, and choose View Selection Source.

Now that you have a good idea about what you are going to implement, let's get to work. It's time for action!

Time for Action—Building the Real-Time SVG Chart
1. Start by creating a new subfolder of the ajax folder, called svg_chart.
2. In the svg_chart folder, create a new file named index.html with the following contents:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html>
<head>
<title>AJAX Realtime Charting with SVG</title>
</head>
<body>
<embed src="chart.svg" width="600" height="450" type="image/svg+xml" />
</body>
</html>

3. Then create a file named chart.svg, and add the following code to it:
<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" onload="init(evt)">
<script type="text/ecmascript" xlink:href="ajaxRequest.js"/>
<script type="text/ecmascript" xlink:href="realTimeChart.js"/>
<a xlink:href="http://ajaxphp.packtpub.com">
<text x="200" y="20">
SVG with AJAX and PHP Demo
</text>
</a>
</svg>

4. Create a file named ajaxrequest.js with the following contents:
// will store reference to the XMLHttpRequest object var xmlHttp = null;

// creates an XMLHttpRequest instance function createXmlHttpRequestObject()
{
// will store the reference to the XMLHttpRequest object
var xmlHttp;
// this should work for all browsers except IE6 and older try
{
// try to create XMLHttpRequest object xmlHttp = new XMLHttpRequest();
}
catch(e)
{
// assume IE6 or older
var XmlHttpVersions = new Array("MSXML2.XMLHTTP.6.0", "MSXML2.XMLHTTP.5.0",
"MSXML2.XMLHTTP.4.0", "MSXML2.XMLHTTP.3.0", "MSXML2.XMLHTTP", "Microsoft.XMLHTTP");
// try every prog id until one works
for (var i=0; i<XmlHttpVersions.length && !xmlHttp; i++)
{
try
{
// try to create XMLHttpRequest object
xmlHttp = new ActiveXObject(XmlHttpVersions[i]);
}
catch (e) {}
}
}
// return the created object or display an error message if (!xmlHttp)
alert("Error creating the XMLHttpRequest object.");
else
return xmlHttp;
}

// initiates an AJAX request function ajaxRequest(url, callback)
{
// stores a reference to the function to be called when the response
// from the server is received
var innerCallback = callback;
// create XMLHttpRequest object when this method is first called if (!xmlHttp) xmlHttp = createXmlHttpRequestObject();
// if the connection is clear, initiate new server request
if (xmlHttp && (xmlHttp.readyState == 4 || xmlHttp.readyState == 0))

{
xmlHttp.onreadystatechange = handleGettingResults;
xmlHttp.open("GET", url, true);
xmlHttp.send(null);
}
else
// if the connection is busy, retry after 1 second setTimeout("ajaxRequest(url,callback)", 1000);

// called when the state of the request changes function handleGettingResults()
{
// move forward only if the transaction has completed if (xmlHttp.readyState == 4)
{
// a HTTP status of 200 indicates the transaction completed
// successfully
if (xmlHttp.status == 200)
{
// execute the callback function, passing the server response
innerCallback(xmlHttp.responseText)
}
else
{
// display error message alert("Couldn't connect to server");
}
}
}
}

5. The bulk of the client-side work is done by RealTimeChart.js:
// SVG namespace
var svgNS = "http://www.w3.org/2000/svg";
// the SVG document handler var documentSVG = null;
// will store the root <g> element that groups all chart elements var chartGroup = null;
// how often to request new data from server?
var updateInterval = 1000;
// coordinates (in pixels) used to translate the chart var x = 50, y = 50;
// chart's dimension (in pixels)
var height = 300, width = 500;
// chart's axis origin
var xt1 = 0, yt1 = 0;
// chart's axis maximum values var xt2 = 50, yt2 = 100;
// number of horizontal and vertical axis divisions var xDivisions = 10, yDivisions = 10;
// default text width and height for initial display (recalculated
// afterwards)
var defaultTextWidth = 30, defaultTextHeight = 20;
// will retain references to the chart units for recalculating positions
var xIndexes = new Array(xDivisions + 1);
var yIndexes = new Array(yDivisions + 1);
// will store the text node that displays the selected chart node
var currentNodeInfo;
// retains the latest values generated by server var lastX = -1, lastY = -1;
// shared svg elements
var chartGroup, dataGroup, dataPath;

// initializes chart function init(evt)
{
/**** Prepare the group that will contain all chart data ****/
// obtain SVG document handler
documentSVG = evt.target.ownerDocument;
// create the <g> element that groups all chart elements chartGroup = documentSVG.createElementNS(svgNS, "g");
chartGroup.setAttribute("transform", "translate(" + x + " " + y + ")");

/**** Prepare the group that will store the Y and Y axis and numbers ****/
axisGroup = documentSVG.createElementNS(svgNS, "g");
// create the X axis line as a <path> element
axisPath = documentSVG.createElementNS(svgNS, "path");
// the axis lines will be black, 2 pixels wide axisPath.setAttribute("stroke", "black"); axisPath.setAttribute("stroke-width", "2");

/**** Create the division lines for the X and Y axis ****/
// create the path definition text for the X axis division lines
pathText = "M 0 " + height;
// adds divisions to the X axis (differently for last division)
for (var i = 0; i <= xDivisions; i++)
pathText += "l 0 5 l 0 -5 " +
((i == xDivisions) ? "" : ("l " + width/xDivisions + " 0"));
// create the path definition text for the Y axis division lines
pathText += "M 0 " + height;
// adds one division to the Y axis (differently for last division)
for (var i = 0; i <= yDivisions; i++)
pathText += "l -5 0 l 5 0 " +
((i == yDivisions) ? "" : ("l 0 -" + height / yDivisions));
// add the path definition (the <d> attribute) to the path
axisPath.setAttribute("d", pathText);
// add the path to the axis group axisGroup.appendChild(axisPath);

/**** Create the text nodes for the X and Y axis ****/
// adds text nodes for the X axis
for (var i = 0; i <= xDivisions; i++)
{
// creates the <text> node for the division
t = documentSVG.createElementNS(svgNS, "text");
// stores the node for future reference xIndexes[i] = t;
// creates the text for the <text> node t.appendChild(documentSVG.createTextNode(
(xt1 + i * ((xt2 - xt1) / xDivisions)).toFixed(1)));
// sets the X and Y attributes for the <text> node t.setAttribute("x", i * width / xDivisions - defaultTextWidth / 2); t.setAttribute("y", height + 30 + defaultTextHeight);
// when the graph first loads, we want the text nodes invisible t.setAttribute("stroke", "white");
// add the <text> node to the axis group
axisGroup.appendChild(t);
}
// adds text nodes for the Y axis
for (var i = 0; i <= yDivisions; i++)
{
// creates the <text> node for the division
t = documentSVG.createElementNS(svgNS, "text");
// stores the node for future reference yIndexes[i] = t;
// creates the text for the <text> node t.appendChild(documentSVG.createTextNode(
(yt1 + i * ((yt2 - yt1) / yDivisions)).toFixed(1)));

// sets the X and Y attributes for the <text> node t.setAttribute("x", -30 -defaultTextWidth); t.setAttribute("y", height - i * height / yDivisions
+ defaultTextHeight / 2);
// when the graph first loads, we want the text nodes invisible
t.setAttribute("stroke", "white");
// add the <text> node to the axis group axisGroup.appendChild(t);
}

// add the axis group to the chart chartGroup.appendChild(axisGroup);

/**** Prepare the <path> element that will draw chart's data ****/ dataPath = documentSVG.createElementNS(svgNS, "path"); dataPath.setAttribute("stroke", "black"); dataPath.setAttribute("stroke-width", "1"); dataPath.setAttribute("fill", "none");
// add the data path to the chart group
chartGroup.appendChild(dataPath);

/**** Final initialization steps ****/
// add the chart group to the SVG document
documentSVG.documentElement.appendChild(chartGroup);
// this is needed to correctly display text nodes in Firefox setTimeout("refreshXYIndexes()", 500);
// initiate repetitive server requests setTimeout("updateChart()", updateInterval);
}

// this function redraws the text for the axis units and makes it visible
// (this is required to correctly position the text in Firefox)
function refreshXYIndexes()
{
// redraw text nodes on the X axis
for (var i = 0; i <= xDivisions; i++)
if (typeof xIndexes[i].getBBox != "undefined")
try
{
textWidth = xIndexes[i].getBBox().width;
textHeight = xIndexes[i].getBBox().height;
xIndexes[i].setAttribute("x", i*width/xDivisions - textWidth/2); xIndexes[i].setAttribute("y", height + 10 + textHeight); xIndexes[i].setAttribute("stroke", "black");
}
catch(e) {}
// redraw text nodes on the Y axis
for (var i = 0; i <= yDivisions; i++)
if (typeof yIndexes[i].getBBox != "undefined")
try
{
twidth = yIndexes[i].getBBox().width;
theight = yIndexes[i].getBBox().height;
yIndexes[i].setAttribute("y", height-i*height/yDivisions
+theight/2);
yIndexes[i].setAttribute("x", -10 -twidth);
yIndexes[i].setAttribute("stroke", "black");
}
catch(e) {}
}

// called when mouse hovers over chart node to display its coordinates function createPointInfo(x, y, whereX, whereY)
{
// make sure you don't display more coordinates at the same time if (currentNodeInfo) removePointInfo();
// create text node
currentNodeInfo = documentSVG.createElementNS(svgNS, "text");

currentNodeInfo.appendChild(documentSVG.createTextNode("("+x+","+y+")"));
// set coordinates
currentNodeInfo.setAttribute("x", whereX.toFixed(1));
currentNodeInfo.setAttribute("y", whereY - 10);
// add the node to the group chartGroup.appendChild(currentNodeInfo);
}

// removes the text node that displays chart node coordinates function removePointInfo()
{
chartGroup.removeChild(currentNodeInfo);
currentNodeInfo = null;
}

// draws a new point on the graph function addPoint(X, Y)
{
// save these values for future reference lastX = X;
lastY = Y;
// start over (reload page) after the last value was generated if (X == xt2)
window.location.reload(false);
// calculate the coordinates of the new node coordX = (X - xt1) * (width / (xt2 - xt1));
coordY = height - (Y - yt1) * (height / (yt2 - yt1));
// draw the node on the chart as a blue filled circle
var circle = documentSVG.createElementNS(svgNS, "circle");
circle.setAttribute("cx", coordX); // X position
circle.setAttribute("cy", coordY); // Y position circle.setAttribute("r", 3); // radius circle.setAttribute("fill", "blue"); // color circle.setAttribute("onmouseover",
"createPointInfo(" + X + "," +
Y + "," + coordX + "," + coordY + ")");
circle.setAttribute("onmouseout", "removePointInfo()");
chartGroup.appendChild(circle);
// add a new line to the new node on the graph
current = dataPath.getAttribute("d"); // current path definition
// update path definition
if (!current || current == "")
dataPath.setAttribute("d", " M " + coordX + " " + coordY);
else
dataPath.setAttribute("d", current + " L " + coordX + " " + coordY);
}

// initiates asynchronous request to retrieve new chart data function updateChart()
{
// builds the query string
param = "?lastX=" + lastX + ((lastY != -1) ? "&lastY=" + lastY : "");
// make the request through either AJAX
if (window.getURL)
// Supported by Adobe's SVG Viewer and Apache Batik getURL("svg_chart.php" + param, handleResults);
else

// Supported by Mozilla, implemented in ajaxRequest.js ajaxRequest("svg_chart.php" + param, handleResults);
}

// callback function that reads data received from server function handleResults(data)
{
// get the response data
if (window.getURL)
responseText = data.content;
else
responseText = data;
// split the pair to obtain the X and Y coordinates var newCoords = responseText.split(",");
// draw a new node at these coordinates addPoint(newCoords[0], newCoords[1]);
// restart sequence
setTimeout("updateChart()", updateInterval)
}

6. Finally, create the server-side script, named svg_chart.php:
<?php
// variable initialization
$maxX = 50; // our max X
$maxY = 100; //our max Y
$maxVariation = $maxY / 7; // maximum Y variation for one step
// client tells last X value generated (defaults to -1)
if (isset($_GET['lastX']))
$lastX = $_GET['lastX'];
else
$lastX = -1;
// client tells last Y value generated (defaults to random)
if (isset($_GET['lastY']))
$lastY = $_GET['lastY'];
else
$lastY = rand(0, $maxY);
// calculate a new random number
$randomY = (int) ($lastY + $maxVariation - rand(0, $maxVariation*2));
// make sure the new Y is between 0 and $maxY
while ($randomY < 0) $randomY += $maxVariation;
while ($randomY > $maxY) $randomY -= $maxVariation;
// generate a new pair of numbers
$output = $lastX + 1 . ',' . $randomY;
// clear the output if(ob_get_length()) ob_clean();
// headers are sent to prevent browsers from caching
header('Expires: Fri, 25 Dec 1980 00:00:00 GMT'); // time in the past header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . 'GMT');
header('Cache-Control: no-cache, must-revalidate');
header('Pragma: no-cache');
// send the results to the client
echo $output;
?>

7. Load http://localhost/ajax/svg_chart, and admire your brand new chart!

What Just Happened?
Let's briefly look at the important elements of the code, starting with the server. The svg_chart.php script is called asynchronously to generate a new set of (X, Y) coordinates to be displayed by the client in the chart. The client is supposed to tell the server the previously generated

coordinates, and based on that data, the server generates a new set. This simulates pretty well a real-world scenario. The previously generated coordinates are sent via GET as two parameters named lastX and lastY. To test the server-side functionality independently of the rest of the solution, try loading http://localhost/ajax/svg_chart/svg_chart.php?lastX=5&lastY=44:

Figure 7.3: The Server generating a New Set of Coordinates for the Client

On the client, everything starts with index.html, which is really simple; all it does is to load the SVG file. The best way to do this at the moment is by using an <embed> element, which isn't supported by W3C (you can also use <object> and <iframe>, but they are more problematic—see http://www.w3schools.com/svg/svg_inhtml.asp):

<body>
<embed src="chart.svg" width="600" height="450" type="image/svg+xml" />
</body>

And here it comes—chart.svg. This file isn't very impressive by itself, because it uses functionality provided by the JavaScript files (notice the onload event), but it includes the chart title:

<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" onload="init(evt)">
<script type="text/ecmascript" xlink:href="ajaxRequest.js"/>
<script type="text/ecmascript" xlink:href="realTimeChart.js"/>
<a xlink:href="http://ajaxphp.packtpub.com">
<text x="200" y="20">
SVG with AJAX and PHP Demo
</text>
</a>
</svg>

chart.svg references two JavaScript files:

ajaxRequest.js contains the engine that implements asynchronous HTTP request functionality using the XMLHttpRequest object. This engine is used by realTimeChart.js to get new chart data from the server when the Firefox web browser is used. For the Adobe SVG and Apache Batik implementations, we use specific functionality provided by these engines through their getURL methods instead. See the updateChart method in realTimeChart.js for details.

The code in ajaxRequest.js contains a different coding pattern than what you've met so far in the book. What is important to understand is:

• All HTTP requests go through a single XMLHttpRequest instance, rather than automatically creating new XMLHttpRequest objects for each call, as implemented in other patterns. This way we are guaranteed to receive responses in the same order as they were requested, which is important for our charting application (and for any other application where the responses must come in order). If the connection is busy processing a previous request, our code waits for one second, and then retries. This technique is also friendly in its use of the web server resources.
• The ajaxRequest() method receives as parameter the URL to connect to, and a reference to the callback function to be called when the server response is received. This is a good technique to implement when you need the flexibility to access the same functionality from several sources.
• The handleGettingResults() method is defined inside the ajaxRequest method.
This is one of JavaScript's features that enable emulating OOP functionality. So far we haven't used these features because we think they bring real benefits only when writing large applications and they require a longer learning curve for programmers inexperienced in OOP techniques. If you like this technique, you'll find it easy to implement it in your applications.

realTimeChart.js contains all the functionality that generates the SVG chart. The code contains detailed comments about the specific techniques. Here is a short description of each of the methods:

• init() is called when the page loads to perform the chart initialization. This method generates the SVG code for the chart axis and builds the initial structure for the whole chart. Initially, the numbers for the axis units are drawn with white font to make them invisible. We need this because of a problem in the Firefox SVG implementation that doesn't allow calculating the text size and positioning it
correctly before it is rendered on the screen. Using pre-calculated values isn't an option because the grid is configurable and its axis can be populated with different values. To overcome this problem, init() uses setTimeout() to execute
refreshXYIndexes() after half a second.
• refreshXYIndexes() is able to calculate the correct positions for the text on the axis units, even with Firefox 1.5. After it sets the new coordinates, it changes the color of the text from white to black, making it visible.
• createPointInfo() is called from the onmouseover function of the chart nodes to display the node coordinates.
• removePointInfo() is called from the onmouseout event to remove the displayed node coordinates.
• updateChart() is the function that initiates the asynchronous request. The getURL method is used if available (this method is supported by Adobe SVG and Apache Batik). Otherwise, the ajaxRequest method (from ajaxRequest.js) is used to make the request. When calling the server, the pair of previously generated coordinates is sent via GET, which the server uses to calculate the new values.

• handleResults() is the callback method that is called by ajaxRequest when the response from the server is received. This response is read (again, with SVG implementation-specific code), and the coordinates generated by the server are sent to addPoint().
• addPoint() receives a set of coordinates used to generate a new node on the chart.
These coordinates are saved for later, because on the next request they will be sent to the server. The server will use these coordinates to calculate the new ones for the client, enabling the simple mechanism of managing state: with each new request the X coordinate is increased by one and the Y is calculated randomly, but with a function that takes into account the previously generated Y coordinate.

Summary
Whether you like SVG or not (especially in the light of the recent SVG versus Flash wars), you must admit it allows implementing powerful functionality in web pages. Having tasted its functionality, you'll now know in which projects you might consider using it. If you are serious about SVG, be sure to check out the visual editors around, which make SVG creation a lot easier. You may also consider purchasing one of the numerous SVG books.

0 comments: