GraphStream UI Tutorial 3 : Visualising data, adding animated graphical elements on the graph view

SourceForge.net Logo

In this tutorial we will learn how to visualise the data that can be associated with the graph. We will see how to put textual and visual informations called "sprites" on nodes and edges. Then we will see how to animate these informations.

A graph image A graph image A graph image A graph image A graph image

Sprites

The term sprite is used in the computer video game field to speak about a little, sometimes animated, graphical element, often in two dimensions, like a cursor or a character, that can be part of a larger scene in a game. Some early computers of old ages had dedicated hardware for rendering sprites as 2D bitmaps on screen.

This term therefore seemed us appropriate for small elements, maybe animated, that we can put on the graph display in order to show some information about a particular algorithm or process occurring on the represented graph.

In GraphStream, a sprite is drawn almost like a node, but it is not connected to others, and may adopt some shapes common to nodes but also other shapes : Sprites can be texts, images, arrows, text boxes, circles, squares, pie-chart, etc.

Their main purpose is to visualise data.

Sprites can be positioned everywhere on the graph view. You then specify the sprite coordinates in two or three dimensions using two or three Cartesian (some say rectangular) coordinates.

But sprites can also be "attached", or tied, to nodes or edges. In this case they are positioned relative to the node or edge origin. For nodes the origin is the centre of the node, for edges, the origin is the centre of the "source" node.

In the case of nodes the positioning is then expressed using polar (2D) or spherical (3D) coordinates around the node. For edges, the positioning is given by a percent of travel along the edge, from the "source" node to the "target" node (even if the edge is not directed, remember that an edge in GraphStream names its two endpoints "from" and "to" or "node0" and "node1" and that this order is conserved).

But lets see an example of these three forms of sprites :

package org.miv.graphstream.ui.swing.test;

import org.miv.graphstream.graph.Graph;
import org.miv.graphstream.graph.Node;
import org.miv.graphstream.graph.implementations.DefaultGraph;
import org.miv.graphstream.ui.GraphViewerRemote;
import org.miv.graphstream.ui.Sprite;

public class TutorialUI003a
{
	public static void main( String args[] )
	{
		new TutorialUI003a();
	}
	
	public TutorialUI003a()
	{
		Graph graph = new DefaultGraph();
		
		GraphViewerRemote remote = graph.display( false );
		
		remote.setQuality( 4 );
		graph.addAttribute( "ui.stylesheet", styleSheet );
		
		Node A = graph.addNode( "A" );
		Node B = graph.addNode( "B" );
		Node C = graph.addNode( "C" );
		
		graph.addEdge( "AB", "A", "B" );
		graph.addEdge( "BC", "B", "C" );
		graph.addEdge( "CA", "C", "A" );
		
		A.addAttribute( "label", "A" );
		B.addAttribute( "label", "B" );
		C.addAttribute( "label", "C" );
		
		A.addAttribute( "x",  0 );
		A.addAttribute( "y",  1 );
		B.addAttribute( "x", -1 );
		B.addAttribute( "y",  0 );
		C.addAttribute( "x",  1 );
		C.addAttribute( "y",  0 );
		
		// The use of sprites begins here.
		
		Sprite S1 = remote.addSprite( "S1" );
		Sprite S2 = remote.addSprite( "S2" );
		Sprite S3 = remote.addSprite( "S3" );
		
		S1.attachToEdge( "AB" );
		S2.attachToNode( "B" );
	}
	
	public static String styleSheet =
		"node { width:16px; color:grey; border-width:1px; border-color:black; text-color:black; }" +
		"edge { color:black; }" +
		"sprite { width:10px; }" +
		"sprite#S1 { color: red; }" +
		"sprite#S2 { color: yellow; }" +
		"sprite#S3 { color: blue; }";
}

The major part of this program is dedicated to the creation of a graph that looks like a triangle. You should now be familiar with it. We did not requested automatic layout of the nodes, and therefore we specify 2D coordinates of each three nodes by ourself.

The interesting part of the program begins after the comment. We create three sprites. They are created using the GraphViewerRemote. Indeed, sprites are only graphical elements, they are not really part of the graph, and therefore are not created inside the graph like nodes and edges.

Then we specify how sprites must position themselves. The first one is attached to an edge and will therefore use this edge as origin of its coordinate system. The second one will use a node as origin. The third sprite is not attached and will be located like the nodes at absolute positions, in the same space.

Also note that sprites can be "styled" like nodes and edges using the style sheet. This will be detailed later.

When running this program you may be surprised (or disappointed) of its output :

A graph image

We specified in the style sheet that the S1 sprite be red, the S2 sprite be yellow and the S3 sprite be blue. By default the shape of a sprite is like the default shape of a node : a simple colored disk. We specified that sprites should be 10 pixels wide, so our disks have a 5 pixel radius. The nodes have a 8 pixels radius and appear under.

Excepted for the attachment, we did not specified positions for our sprites. So let see how GraphStream position them.

Edge attachment

The red sprite is attached to the "AB" edge. When a sprite is attached to an edge, it is always positioned along this edge. By default a sprite attached to an edge uses only one x coordinate as an offset on the edge. This coordinate must be comprised in the range [0,1] (if it is not, GraphStream will clip it to the closest bound).

Therefore, a sprite located at coordinate 0.5 will be exactly in the middle of the edge. The figure under illustrates how this work. The x coordinates acts as a vector pushing the sprite from position 0 that is aligned on the source node (here a node whose identifier is A) from an amount proportional to the size of the edge, here 40% of the edge :

A graph image

Now we know why our sprite is exactly above the A node. We did not specified a position for it, and it is at 0% of the edge.

When the graph is rendered in two dimensions, sprites can use a second y coordinate that can be used to offset the sprite perpendicular to the position x on the edge. The x coordinates units are defined along the edge, but the units in which the y coordinate is expressed can be chosen. By default they are pixels. This choice has been done because pixels are constant and therefore the spacing between the sprite and the edge will not vary if the window is resized. However you can use graph units (gu) or percents (%) if you wish. This is shown on the figure under for a distance of 5 pixels :

A graph image

You choose the side of the edge using the sign of the distance.

The third coordinate can also be used. However it is useful only when the graph is rendered in three dimensions. This coordinate allows to rotate the sprite around the edge axis and is therefore an angle whose units are radians.

Node attachment

The yellow sprite is attached to node B. When a sprite is tied to a node, the origin of its coordinate system is the node centre.

As for sprites attached to edges, you can move the sprite around the node. This is done using either polar coordinates when in two dimensions, or spherical coordinates when using three dimensions.

In two dimensions, the sprite is positioned using a radius that gives the distance from the node centre to the sprite centre and an angle phi that gives a rotation in radians around the node centre. The angle "origin" is at three o'clock and the rotation direction is counter-clockwise. This means that when only the radius is given and phi is zero, the sprite is on the right of the node.

For the angle the value is always interpreted as radians. For the radius you can choose the units you want. By default they are pixels.

When the graph is rendered in three dimensions, the other coordinate is a theta angle that allows to position the sprite on a sphere around the node. The figure under shows this coordinate system :

A graph image

GraphStream uses a "left handed" coordinate system. This means that the X positive axis points right, the Y positive axis points up and the Z positive axis points toward the viewer ("out of the screen"). The phi angle is expressed on the XY plane and the theta angle on the XZ plane.

As said above, in two dimensions, only the phi angle is used.

As we did not specified the coordinates for our yellow sprite, it is located at a radius of 0 and phi angle of 0, which means that it is aligned with the origin of its coordinate system and therefore is on its node centre.

Here is a figure that show how positioning occurs for a sprite that is 10 pixels from its node centre and at phi angle 30° :

A graph image

No attachment

When a sprite is not attached to a node or edge its positioning uses two or three coordinates (in 2D or 3D) in the Cartesian (or rectangular) space. The units are always graph units, that is the units you use to specify node positions.

As we did not specified coordinates for our non-attached sprite. It is located at (0,0) in graph units. It may seem attached on the edge "BC" but this is only by chance.

Specifying positions

Now let see how to specify the positions in our example. Add the following lines at the end :

		S1.position( 0.4f );
		S2.position( 20, 0, 0, Style.Units.PX );
		S3.position( 0, 0.5f, 0 );

Here is what we get :

A graph image

We moved the red sprite from 40% on the edge "AB". We positioned the yellow sprite at a 20 pixels radius from the node "B". We positioned the blue sprite at (0,0.5) absolute coordinates in graph units.

There are three position() methods in the Sprite class. The first one is dedicated to sprites on edges, as it takes only one coordinate. The second takes three arguments x, y and z. This method allows to position sprites on edges, on nodes or at absolute positions. The third version takes a fourth argument that specify the units of some of the lengths used.

The units are Style.Units.PX, Style.Units.GU and Style.Units.PERCENTS. They are used to interpret radius and edge offset.

Now modify the three lines above like this :

		S1.position( 0.4f, 15, 0, Style.Units.PX );
		S2.position( 20, 0, (float)Math.PI, Style.Units.PX );
		S3.position( 0, 0.5f, 0 );

To get this :

A graph image

How exciting !! We specified a red sprite at 40% on the edge, and at 15 pixels perpendicular to this position on the edge. We specified the yellow sprite at a 20 pixel radius of the node "B" centre and at 180° around "B".

When specifying positions along an edge the x argument is the percent of the edge, the y is the offset from perpendicular to the x position. Eventually, in 3D, the z argument is the phi angle around the edge axis.

When specifying positions around a node the x argument is the radius from the node centre, y is the theta angle and z is the phi angle. This is why we used coordinates (20,0,PI) and not (20,PI,0) to position our sprite in 2D.

When specifying non attached positions, x, y and z arguments are used as coordinates in the Cartesian space.

Now that you know, we can start to do some more funny things like animating the sprites and making them a bit more sexy.

Animating sprites

Animating sprites on the graph may be useful for several tasks, for example :

  • You want to show moving markers, labels or tags on the edge ;
  • You want to represent a varying amount of something on the edges or nodes 
  • You are creating a vehicle and circulation network simulation and want to represent cars ;
  • You are designing a ant colony algorithm and want to represent ants travelling on the graph ...

There is no method for animating sprites automatically ! This would be difficult since the role of sprites is to represent your data, and you are the only one to know how they may move on the graph.

However it is very easy to specify sprite movement using the various position() methods, or better, by subclassing the Sprite class to your needs. Here is a way to do this.

We propose you to implement a sprite that would move from edge to edge, traveling along the edges it crosses. To do this we will subclass the Sprite class to create a MovingSprite, that is a sprite where we will add a move() method. Its purpose is to compute the next position along an edge, and if it reached the edge end, to choose another edge to cross.

However, Sprite is an interface. We cannot directly subclass it. This interface has two implementations : RemoteSprite and DirectSprite. The first one is used when you use the GraphViewerRemote. The second one is used when you use directly the viewer implementation (for example SwingGraphViewer). This last implementation is useful when your graph code runs in the thread of the viewer (for example in the Swing thread). However this is not often the case. Here we will therefore use the RemoteSprite implementation.

import java.util.Iterator;

import org.miv.graphstream.graph.Edge;
import org.miv.graphstream.graph.Node;
import org.miv.graphstream.graph.Graph;
import org.miv.graphstream.graph.implementations.DefaultGraph;
import org.miv.graphstream.ui.GraphViewerRemote;
import org.miv.graphstream.ui.RemoteSprite;

public class TutorialUI003b
{
	public static void main( String args[] )
	{
		new TutorialUI003b();
	}
	
	public TutorialUI003b()
	{
		Graph graph = new DefaultGraph();
		
		GraphViewerRemote viewerRemote = graph.display();
		
		viewerRemote.setQuality( 4 );
		
		// A simple triangle graph.
		
		Node A  = graph.addNode( "A" );
		Node B  = graph.addNode( "B" );
		Node C  = graph.addNode( "C" );
		
		graph.addEdge( "AB", "A", "B" );
		graph.addEdge( "BC", "B", "C" );
		graph.addEdge( "CA", "C", "A" );

		graph.addAttribute( "ui.stylesheet", styleSheet );
		
		A.addAttribute( "label", "A" );
		B.addAttribute( "label", "B" );
		C.addAttribute( "label", "C" );
		
		// We add one moving sprite.
		
		MovingSprite S1 = new MovingSprite( graph.getEdge( "AB" ), "S1", viewerRemote );

		// We animate the sprite 25 times per second.
		
		while( true )
		{
			S1.move();
			try{ Thread.sleep( 40 ); } catch( Exception e ) {}
		}
	}
	
	/**
	 * Our own sprite class.
	 */
	protected static class MovingSprite extends RemoteSprite
	{
		Edge current;
		float pos = 0;
	
		/**
		 * The constructor needs and edge to start from, the sprite identifier
		 * and the graph viewer remote to communicate with the viewer.
		 */
		public MovingSprite( Edge start, String id, GraphViewerRemote remote )
		{
			super( id, remote );
			current = start;
			attachToEdge( current.getId() );
		}
	
		/**
		 * Move the sprite along the current edge. If the end of the edge is
		 * reached, another edge is chosen.
		 */
		public void move()
		{
			position( pos );
			
			pos += 0.01f;
			
			if( pos >= 1 )
			{
				pos = 0;
				
				Node node = current.getNode1();
				Iterator<? extends Edge> i = node.getEdgeIterator();
				
				while( i.hasNext() )
				{
					Edge edge = i.next();

					if( edge != current )
					{
						current = edge;
						attachToEdge( current.getId() );
						position( pos );
						break;
					}
				}
			}
		}
	}
	
// Constants
		
		protected static String styleSheet =
				  "node {"
				+		"width:16;"
				+		"color:lightgrey;"
				+		"border-width:1;"
				+		"border-color:black;"
				+		"text-color:black;"
				+ "}"
				+ "sprite {"
				+		"color:red;"
				+		"border-width:1;"
				+		"border-color:black;"
				+		"width:10;"
				+		"text-align:aside;"
				+		"text-color:darkgrey;"
				+ "}";
}

The result is difficult to show on a single image. You should see a moving red disk that travels along edges of the triangle graph smoothly.

A graph image

Now that we have such a moving sprite class, it is easy to add more sprites on the edges, and to vary their speed and look.

As an exercise, you could try to do the same with a sprite orbiting around a node.

Sprite shapes

The style sheet is able to specify styles for sprites the same way it does for edges. This allows to represent your data more appropriately than will simple colored disks.

Texts, images and text boxes

Add nodes and edges, sprites supports attributes. One of the most useful attribute of the sprite is label. As you can put sprites virtually everywhere on the graph, you can put text, annotations, labels, tags everywhere.

To add an attribute use the addAttribute() method :

sprite.addAttribute( "label", "This is a sprite !" );

All the text properties of style sheets are usable. In addition you can specify a special property for sprites : sprite-shape. This works exactly the same as node-shape and edge-shape.

Two of the available shapes are text-box and text-ellipse. The are identical to the node shapes of the same name. They draw the label attribute of the sprite and draw a rectangle or ellipse using the color and border properties around the text. If there is an image, the image is put aside the text.

Here is an example of what is possible :

A graph image

And here is the style sheet used :

graph {
	color: grey;
}
node {
	node-shape: text-box;
	border-width: 1px;
	border-color: black;
	color: #E0E0E088;
}
node#A {
	image:url('http://www.iconarchive.com/icons/pino/grimm/Grimm-Head-icon.gif');
}
node#B {
	image:url('http://www.iconarchive.com/icons/pino/grimm/Attila-Side-icon.gif');
}
node#C {
	node-shape:text-ellipse;
	image:url('http://www.iconarchive.com/icons/pino/grimm/Mother-Goose-1-icon.gif');
}
node#D {
	node-shape:text-ellipse;
	image:url('http://www.iconarchive.com/icons/pino/grimm/Sumo-icon.gif');
}
sprite {
	sprite-shape:text-box;
}
sprite#X {
	image:url('http://www.iconarchive.com/icons/pino/grimm/Whiz-2-icon.gif');
	text-align:left;
}
sprite#Y {
	image:url('http://www.iconarchive.com/icons/pino/grimm/Lassie-icon.gif');
	text-align:right;
}

The two sprites use text boxes and use the text-align style attribute to align the box with the edge center. One is on the left of the edge, the other is on the right.

Note that when using the text-box or text-ellipse, the width style attribute is not used since it is derived from the text.

Arrows

One of the possible sprite shapes is arrow. Arrows allows to point at positions on an edge or node. In addition of this shape, you can use the sprite-orientation style attribute to specify what is pointed at by the sprite arrow. This style attribute can take three values :

  • from only usable when the sprite is attached on an edge. This makes the arrow point at the source node of the edge ;
  • to only usable when the sprite is attached on an edge. This makes the arrow point at the target node of the edge ;
  • origin usable for sprites attached to node or edges. This makes the arrow point at the center of the node if the sprite is attached to a node, or point at the sprite position on the edge is the sprite is attached on an edge.

Here is an example :

A graph image

And here is the program that generates this output :

import org.miv.graphstream.graph.Graph;
import org.miv.graphstream.graph.implementations.DefaultGraph;
import org.miv.graphstream.ui.GraphViewerRemote;
import org.miv.graphstream.ui.Sprite;
import org.miv.graphstream.ui.graphicGraph.stylesheet.Style;

public class TutorialUI003e
{
	public static void main( String args[] )
	{
		new TutorialUI003e();
	}
	
	public TutorialUI003e()
	{
		Graph graph = new DefaultGraph( "Arrows", false, true );
		
		GraphViewerRemote remote = graph.display();
		
		remote.setQuality( 4 );
		
		graph.addEdge( "AB", "A", "B" );
		graph.addEdge( "BC", "B", "C" );
		graph.addEdge( "CA", "C", "A" );
		
		graph.getNode("A").addAttribute( "label", "A" );
		graph.getNode("B").addAttribute( "label", "B" );
		graph.getNode("C").addAttribute( "label", "C" );
		
		Sprite arrow1 = remote.addSprite( "arrow1" );
		Sprite arrow2 = remote.addSprite( "arrow2" );
		Sprite arrow3 = remote.addSprite( "arrow3" );
		Sprite arrow4 = remote.addSprite( "arrow4" );
		
		arrow1.attachToEdge( "AB" );
		arrow2.attachToEdge( "AB" );
		arrow3.attachToNode( "A" );
		arrow4.attachToEdge( "BC" );
		arrow1.position( 0.4f );
		arrow2.position( 0.6f );
		arrow3.position( 28, 0, (float)(Math.PI/4), Style.Units.PX );
		arrow4.position( 0.5f, 16, 0, Style.Units.PX );
		
		arrow3.addAttribute( "label", "Here" );
		arrow4.addAttribute( "label", "There" );
		
		graph.addAttribute( "ui.stylesheet", styleSheet );
	}
	
	protected static String styleSheet = 
		"graph { background-color: white; }" +
		"node { width: 18px; color: #3050F0; border-width: 2px; border-color: black; text-color: white; }" +
		"edge { width: 1px; color: black; }" +
		"sprite { sprite-shape: arrow; border-width: 1px; text-align: aside; }" +
		"sprite#arrow1 { color: #F02020; sprite-orientation: from; }" +
		"sprite#arrow2 { color: #20F020; sprite-orientation: to; }" +
		"sprite#arrow3 { color: #F0C020; sprite-orientation: origin; }" +
		"sprite#arrow4 { color: #F020F0; sprite-orientation: origin; }";
}

Flows

Flows allows to express a quantity on an edge. A flow is a line that goes from one edge end (depending on the sprite-orientation style element) to the position of the sprite on the edge.

Here is a small program and its style sheet that shows a possible use of flow sprites :

import org.miv.graphstream.graph.Graph;
import org.miv.graphstream.graph.implementations.DefaultGraph;
import org.miv.graphstream.ui.GraphViewerRemote;
import org.miv.graphstream.ui.Sprite;
import org.miv.graphstream.ui.graphicGraph.stylesheet.Style;

/**
 * How to use flow sprites.
 * 
 * @author Antoine Dutot
 */
public class TutorialUI003c
{
	public static void main( String args[] )
	{
		new TutorialUI003c();
	}
	
	public TutorialUI003c()
	{
		Graph graph = new DefaultGraph( "Flows", false, true );
		
		GraphViewerRemote remote = graph.display();
		
		remote.setQuality( 4 );
		
		graph.addEdge( "AB", "A", "B" );
		graph.addEdge( "BC", "B", "C" );
		graph.addEdge( "CA", "C", "A" );
		
		Sprite flow1 = remote.addSprite( "flow1" );
		Sprite flow2 = remote.addSprite( "flow2" );
		
		flow1.attachToEdge( "AB" );
		flow2.attachToEdge( "AB" );
		flow1.position( 0.4f,  3, 0, Style.Units.PX );
		flow2.position( 0.8f, -3, 0, Style.Units.PX );
		
		graph.addAttribute( "ui.stylesheet", styleSheet );
	}
	
	protected static String styleSheet = 
		"graph { background-color: #707070; }" +
		"node  { width: 12px; color: #C0C0C0; border-width: 3px; border-color: black; }" +
		"edge  { width: 3px; color: black; }" +
		"sprite { width: 5px; sprite-shape: flow; sprite-orientation: from; z-index:-1; }" +
		"sprite#flow1 { color: #C03030; }" +
		"sprite#flow2 { color: #C0C030; }";
}

The result is shown under :

A graph image

For flow sprite to work, they must be attached to edges. The position gives the advance of the flow on the edge. The sprite-orientation properties tells the sprite start point. Only to and from values are possible (other are considered equal to from).

Here we use the z-index style element to make the sprites appear under the edges and nodes, which is cleaner. By default sprites are always drawn above all graph elements.

When positioning the sprite, the offset of the edge offsets the whole sprite line. We used this to put the two flows around the edge, one on each side (at offset 3 and -3).

Pie charts

Pie charts are a sprite shape that allows to show several values attached to a node or edge. They will allow us to show how to declare array-like attribute values on nodes, edges and sprites, as well as a more advanced use of the color property.

To specify a pie-chart you have to define two additional things:

  • The individual colors of each pie part ;
  • The individual proportion occupied by each pie part.

When you specify the color of an element in the style sheet, you usually pass one color. However it is possible to give a list of colors by merely putting several color names or definitions separated by spaces.

This features is useful for multi-coloured elements like pie-charts, but also for gradients.

It is easy to specify the individual proportions of each pie part using the Sprite.addAttribute() method. This method as its counterpart for graph elements, take a first argument that describes the attribute name, and then can take zero, one or more values for this attribute. By default, if no attributes are given, a boolean value "true" is stored in the attribute. If there are several values, an array is created to stored these values.

This is handy because we use the variable argument list feature of Java 1.5 and allow the easy creation of array values for attributes. This is what is used for pie-charts. You specify the individual pie part proportion using the pie-values attribute.

Pie-charts can be attached to nodes, edges or not attached. In the example under, we attach two pie-charts to edges, and one to a node. One takes three values, the other two and the last only one value. When a pie-chart has only one value it circles is not complete.

import org.miv.graphstream.graph.Graph;
import org.miv.graphstream.graph.implementations.DefaultGraph;
import org.miv.graphstream.ui.GraphViewerRemote;
import org.miv.graphstream.ui.Sprite;
import org.miv.graphstream.ui.graphicGraph.stylesheet.Style;

public class TutorialUI003d
{
	public static void main( String args[] )
	{
		new TutorialUI003d();
	}
	
	public TutorialUI003d()
	{
		Graph graph = new DefaultGraph( "Pie-charts", false, true );
		
		GraphViewerRemote remote = graph.display();
		
		remote.setQuality( 4 );
		
		graph.addEdge( "AB", "A", "B" );
		graph.addEdge( "BC", "B", "C" );
		graph.addEdge( "CA", "C", "A" );
		
		graph.getNode("A").addAttribute( "label", "A" );
		graph.getNode("B").addAttribute( "label", "B" );
		graph.getNode("C").addAttribute( "label", "C" );
		
		Sprite pc1 = remote.addSprite( "pc1" );
		Sprite pc2 = remote.addSprite( "pc2" );
		Sprite pc3 = remote.addSprite( "pc3" );
		
		pc1.attachToEdge( "AB" );
		pc2.attachToNode( "A" );
		pc3.attachToEdge( "BC" );
		pc1.position( 0.4f );
		pc2.position( 28, 0, 0, Style.Units.PX );
		pc3.position( 0.5f );
		pc1.addAttribute( "pie-values", 0.1f, 0.3f, 0.6f );
		pc2.addAttribute( "pie-values", 0.4f, 0.6f );
		pc3.addAttribute( "pie-values", 0.4f );
		
		graph.addAttribute( "ui.stylesheet", styleSheet );
	}
	
	protected static String styleSheet = 
		"graph { background-color: white; }" +
		"node  { width: 18px; color: yellow; border-width: 2px; border-color: black; text-color: black; }" +
		"edge  { width: 1px; color: black; }" +
		"sprite { width: 30px; sprite-shape: pie-chart; border-width: 1px; }" +
		"sprite#pc1 { color: #F02020AA #20F020AA #2020F0AA; }" +
		"sprite#pc2 { color: #F0C020AA #20F0F0AA; }" +
		"sprite#pc3 { color: #F020F0AA; }";
}

Here is what you should see :

A graph image

css xhtml