[home]   [me]   [my gallery]   [my blog]   [my articles]   [my code and projects]   [my links]
 

Pie chart revisited



Eight months ago I wrote a small Pie Chart Graph applet in Scenegraph (version 0.4). Since then, I have updated the applet to the newest build of Scenegraph and used it as an example in my Scenegraph Shell applications (See my blogs Scenegraph Shell and Scenegraph Shell 0.2).

The goal of the Pie Chart applet was to create a simple application that looked (and behaved) awesome, created using well structured Java code and the Scenegraph project.

The initial version didn't fulfill my expectations completely, so I have created an improved version.

In this blog I'll show the result and talk a bit about implementation details.

The applet

The applet now supports dynamic data. This means that you can actually use the applet to something useful without any changes to the source.

You set the dynamic data using the following applet parameters:

values
In the format Mondays=321:Tuesdays=399:Wednesdays=208:
Thursdays=109:Fridays=609:Saturdays=709:Sundays=582
header
Title of the applet
numberformat
(tool tip number formatter) such as '0 hits'. See java.text.DecimalFormat for more format specification.

Html source for the applet above:


    
    
    



Things I have learned

  • Even in small applications, you should try create independent self contained objects. By doing this, you can focus on one part at the time, and test the object without interference from other part. But the real benefit in doing this comes when writing visual effect code such as mouse over effects, animations, etc. And when writing visual effects is easy, it gives the programmer a freedom to do some really cool stuff.
  • Provide an empty default constructor. This allows you to work visually see what you are coding using the Scenegraph Shell. However for the Scenegraph Shell to be fully functional it needs to integrated into an IDE as a plug-in.

Source code:

The source code is written in Java 1.5. For the same reason I use the Apache Batik, as this framework contains a nice Radial Gradient Paint class (similar to the one introduced in Java 1.6).

The full source code is listed below, is also available in a zip-file (with the required libraries):

Source: piechart_0.2_src.zip
Bin: piechart_0.2.jar

PieChart.java

The PieChart class is responsible for setting up the graph, doing the initial rotation and providing tooltips.

package com.mortennobel.demo.scenariodemo.piechart;

import com.sun.scenario.animation.*;
import com.sun.scenario.animation.util.MouseTrigger;
import com.sun.scenario.animation.util.MouseTriggerEvent;
import com.sun.scenario.scenegraph.*;
import com.sun.scenario.scenegraph.event.SGMouseAdapter;

import java.util.Map;
import java.awt.*;
import java.awt.geom.*;
import java.awt.event.MouseEvent;
import java.text.DecimalFormat;
import java.text.NumberFormat;

public class PieChart extends SGGroup{
private Evaluator<Double> doubleEvaluator = Evaluators.getLinearInstance(Double.class);

private SGNode currentTooltipNode = null;
private SGComposite toolTipComposite = new SGComposite();

private Timeline timeline = new Timeline();
private final Dimension dimension;

public PieChart(Dimension dimension, String header,Map<String, Double> data, String numberFormat) {
this.dimension = dimension;
toolTipComposite.setMode(SGComposite.Mode.SRC_OVER);
NumberFormat numberFormatter = new DecimalFormat(numberFormat);
SGNode node = createPieChart(data, header,numberFormatter);

add(node);
timeline.start();
}

private TimingTarget mouseOverTimingTarget = new TimingTargetAdapter(){
public void timingEvent(float fraction, long duration) {
toolTipComposite.setOpacity(fraction);
}
};

private SGNode createPieChart(Map data, String headline, NumberFormat numberFormatter){
SGGroup group = new SGGroup();
try{
double sum = 0;
for (Object name:data.keySet()){
sum += (Double)data.get(name);
}

// add headline
SGText headlineNode = createHeadline(headline);
group.add(headlineNode);

SGNode createTooltiptext = createTooltiptext("");
final SGTransform.Translate tooltipTranslation = SGTransform.createTranslation(0,0,createTooltiptext);
tooltipTranslation.setChild(toolTipComposite);
toolTipComposite.setOpacity(0);

Timeline rotationTimeline = new Timeline();

double currentsum = 0;
for (Object nameObj:data.keySet()){
String name = (String)nameObj;
final double value = (Double)data.get(name);

final SGText labelNode = createLabelNode(name,Math.PI*2*((currentsum+value/2)/sum));
group.add(labelNode);

final String tooltipString = numberFormatter.format(value);

// create arc shape
final PieChartArc arcShape = new PieChartArc(sum, value, currentsum);

arcShape.addMouseListener(new SGMouseAdapter(){
public void mouseEntered(MouseEvent e, SGNode node) {
toolTipComposite.setChild(createTooltiptext(tooltipString));
currentTooltipNode = node;
}

public void mouseExited(MouseEvent e, SGNode node) {
if (node == currentTooltipNode){
currentTooltipNode = null;
}
}

public void mouseMoved(MouseEvent e, SGNode node) {
tooltipTranslation.setTranslation(e.getX()-dimension.width/2,e.getY()-dimension.height/2-20);
}
});

// create rotation effect
// start with a negative rotation half of the angle
final double sourceAngle = 0;
final double destAngle = 2*Math.PI*((currentsum/sum)+(value/2/sum));
final SGTransform.Rotate rotationNode = SGTransform.createRotation(sourceAngle,arcShape);
Clip rotationClip = Clip.create(2000, new TimingTargetAdapter(){
public void timingEvent(float fraction, long duration) {
rotationNode.setRotation(doubleEvaluator.evaluate(sourceAngle, destAngle, fraction));
}
});
rotationTimeline.schedule(rotationClip,1000);

group.add(rotationNode);

currentsum += value;
}

group.add(tooltipTranslation);

Clip tooltipMouseOverClip = Clip.create(500, mouseOverTimingTarget);
tooltipMouseOverClip.setAutoReverse(true);
final MouseTrigger tooltipMouseTrigger = new MouseTrigger(tooltipMouseOverClip, MouseTriggerEvent.ENTER);

group.addMouseListener(new SGMouseAdapter(){
public void mouseEntered(MouseEvent e, SGNode node) {
tooltipMouseTrigger.mouseEntered(e);
}

public void mouseExited(MouseEvent e, SGNode node) {
tooltipMouseTrigger.mouseExited(e);
}
});

timeline.schedule(rotationTimeline);
}
catch (Exception e){
e.printStackTrace();
}
return SGTransform.createTranslation(dimension.width/2,dimension.height/2,group);
}

private SGText createHeadline(String headline) {
SGText headlineNode = new SGText();
headlineNode.setFont(new Font("Dialog",Font.BOLD, 20));
headlineNode.setFillPaint(Color.WHITE);
headlineNode.setVerticalAlignment(SGText.VAlign.TOP);
headlineNode.setText(headline);
headlineNode.setAntialiased(true);
headlineNode.setLocation(new Point((int)-headlineNode.getBounds().getWidth()/2,-180));
return headlineNode;
}

private SGNode createTooltiptext(String value){
SGText textShape = new SGText();
textShape.setFont(new Font("Dialog",Font.PLAIN, 14));
textShape.setText(value);
textShape.setAntialiased(true);
int margin = 4;
Rectangle2D bounds = textShape.getBounds();

SGShape sgTextLineShape = new SGShape();
sgTextLineShape.setShape(new RoundRectangle2D.Double(-margin+bounds.getX(),-margin+bounds.getY(),bounds.getWidth()+2*margin,bounds.getHeight()+2*margin,10,10));
sgTextLineShape.setDrawPaint(Color.gray);
sgTextLineShape.setFillPaint(Color.lightGray);
sgTextLineShape.setAntialiasingHint(RenderingHints.VALUE_ANTIALIAS_ON);
sgTextLineShape.setMode(SGAbstractShape.Mode.STROKE_FILL);
SGGroup sgGroup = new SGGroup();
sgGroup.add(sgTextLineShape);
sgGroup.add(textShape);
return sgGroup;
}

SGText createLabelNode(String text, double angleRadian){
SGText sgText = new SGText();
sgText.setText(text);
sgText.setFont(new Font("Dialog",Font.PLAIN,12));
sgText.setAntialiased(true);
Rectangle2D rect2D = sgText.getBounds();
double x = -rect2D.getCenterX();
double y = -rect2D.getCenterY();
double cosAngle = Math.cos(angleRadian);
double sinAngle = Math.sin(angleRadian);
if (cosAngle<-0.5){
x += -rect2D.getWidth()/2;
}
else if (cosAngle>0.5){
x += rect2D.getWidth()/2;
}
if (sinAngle<-0.5){
y += -rect2D.getHeight()/2;
}
else if (sinAngle>0.5){
y += rect2D.getHeight()/2;
}

x+=cosAngle*135;
y+=sinAngle*135;
sgText.setLocation(new Point2D.Double(x,y));
return sgText;
}
}

PieChartBackground.java

This class creates a simple background object with a radial gradient paint fill.

package com.mortennobel.demo.scenariodemo.piechart;

import com.sun.scenario.scenegraph.SGRenderCache;
import com.sun.scenario.scenegraph.SGGroup;
import com.sun.scenario.scenegraph.SGShape;
import com.sun.scenario.scenegraph.SGAbstractShape;

import java.awt.*;

import org.apache.batik.ext.awt.RadialGradientPaint;

public class PieChartBackground extends SGRenderCache {
public PieChartBackground() {
this(new Dimension(500,500)); // for testing in scenegraph shell
}

public PieChartBackground(Dimension background) {
setID("PieChartBackground");
SGGroup group = new SGGroup();
SGShape shape = new SGShape();
shape.setShape(new Rectangle(background.width, background.height));
shape.setMode(SGAbstractShape.Mode.FILL);
int distance = (int)Math.sqrt(Math.pow(0.75*background.height,2)+Math.pow(background.width/2,2));
shape.setFillPaint(new RadialGradientPaint(background.width/2,background.height*0.25f, distance, new float[]{0,1},new Color[]{new Color(0x5798bf),new Color(0x22323c)}));
group.add(shape);
setChild(group);
}
}

PieChartArc.java

The arc is responsible for the color and shape of the arc. Besides I have added a new feature, that let you click on a arc to highlight it (it will then move away from the chart center point).

package com.mortennobel.demo.scenariodemo.piechart;

import com.sun.scenario.scenegraph.*;
import com.sun.scenario.scenegraph.event.SGMouseAdapter;
import com.sun.scenario.scenegraph.event.SGMouseListener;
import com.sun.scenario.animation.*;
import com.sun.scenario.animation.util.MouseTrigger;
import com.sun.scenario.animation.util.MouseTriggerEvent;

import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.geom.Arc2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.AffineTransform;

import org.apache.batik.ext.awt.RadialGradientPaint;


public class PieChartArc extends SGGroup {
private Evaluator<Color> colorEvaluator = Evaluators.getLinearInstance(Color.class);
private Evaluator<Double> doubleEvaluator = Evaluators.getLinearInstance(Double.class);

private static final int CLICK_MOVEMENT_DISTANCE = 25;

private static final Color DRAW_COLOR = Color.gray;
private static final Color DRAW_COLOR_HIGHLIGHTED = Color.WHITE;

private final Color color;

private SGShape shape = new SGShape();

private float extended = 0;

private final MouseTrigger mouseTrigger;
private final SGTransform.Translate clickTranslation;

private TimingTarget mouseOverEffect = new TimingTargetAdapter(){
public void timingEvent(float fraction, long duration) {
fraction = Math.max(fraction, extended);
fraction /=2;
final RadialGradientPaint radialGradientPaint = radialGradientPaint(color, 0.5f+fraction);
shape.setFillPaint(radialGradientPaint);
}
};

private SGMouseListener mouseListener = new SGMouseAdapter(){
public void mouseExited(MouseEvent e, SGNode node) {
mouseTrigger.mouseExited(e);
}

public void mouseEntered(MouseEvent e, SGNode node) {
mouseTrigger.mouseEntered(e);
}

private Clip movementAnimation = null;

public void mouseClicked(MouseEvent e, SGNode node) {
if (movementAnimation!=null && movementAnimation.isRunning()){
movementAnimation.cancel();
}

final boolean moveOut = clickTranslation.getTranslateX()==0;
final double source = clickTranslation.getTranslateX();
final double dest = moveOut? CLICK_MOVEMENT_DISTANCE :0;
movementAnimation = Clip.create((int)Math.abs(source-dest)*10, new TimingTargetAdapter(){
public void timingEvent(float v, long l) {
clickTranslation.setTranslateX(doubleEvaluator.evaluate(source,dest, v));

if (!moveOut){
v = 1-v;
}
extended = v;
shape.setDrawPaint(colorEvaluator.evaluate(DRAW_COLOR, DRAW_COLOR_HIGHLIGHTED, v));
}

public void end() {
clickTranslation.setTranslateX(dest);
}
});
movementAnimation.start();
}
};

public PieChartArc(){
this(10,3,3); // for testing purpose
}

public PieChartArc(double sum, double value, double currentsum) {
double start = 0;
double end = 360*value/sum;
Shape arcShape = new Arc2D.Double(new Rectangle2D.Float(-100,-100,200,200),start,end, Arc2D.PIE);
arcShape = AffineTransform.getRotateInstance(Math.PI*value/sum).createTransformedShape(arcShape);

shape.setShape(arcShape);
shape.setDrawPaint(Color.gray);
shape.setAntialiasingHint(RenderingHints.VALUE_ANTIALIAS_ON);
shape.setMode(SGAbstractShape.Mode.STROKE_FILL);

color = fractionToColor((float)(currentsum/sum));
RadialGradientPaint gradientPaint = radialGradientPaint(color,0.5f);
shape.setFillPaint(gradientPaint);

// create mouse over effect
Clip mouseOverClip = Clip.create(500, mouseOverEffect);
mouseOverClip.setAutoReverse(true);
mouseTrigger = new MouseTrigger(mouseOverClip, MouseTriggerEvent.ENTER);

clickTranslation = SGTransform.createTranslation(0,0, shape);
add(clickTranslation);

addMouseListener(mouseListener);
setCursor(new Cursor(Cursor.HAND_CURSOR));
}

private RadialGradientPaint radialGradientPaint(Color color, float transparency){
return new RadialGradientPaint(0,0,100,new float[]{0,1},new Color[]{deriveColor(color,transparency), deriveColor(color.brighter(),transparency-0.3f)});
}

private Color deriveColor(Color color, float transparency){
float[] colorFractions = color.getComponents(null);
return new Color(colorFractions[0],colorFractions[1],colorFractions[2],transparency);
}

/**
* Idea let 0-1 cycle through Red (0/3), Green (1/3), Blue (2/3) and back to Red (3/3)
* @param fraction color 'degree'
* @return Color resulting color
*/
private Color fractionToColor(float fraction){
Color from;
Color to;
if (fraction<1/3f){
from = Color.red;
to = Color.green;
}
else if (fraction<2/3f){
from = Color.green;
to = Color.blue;
}
else {
from = Color.blue;
to = Color.red;
}
float colorFraction = (fraction%(1/3f))*3f;
return colorEvaluator.evaluate(from,to,colorFraction);
}
}

Ant build script

<project name="Pie Chart" default="jar">

<target name="compile">
<delete dir="bin-pie" />
<mkdir dir="bin-pie" />
<javac source="1.5" srcdir="src_1_5" classpath="libs/batik-all.jar;libs/scenario-0.6x.jar" includes="**/piechart/*.java" destdir="bin-pie" debug="true" optimize="true" verbose="false">
</javac>
</target>

<target name="jar" depends="compile">
<delete dir="dist"/>
<mkdir dir="dist"/>
<jar basedir="bin-pie" destfile="dist/piechart_0.2.jar">
<manifest>
<attribute name="Built-By" value="${user.name}"/>
<attribute name="Implementation-Vendor" value="Morten Nobel-Jørgensen"/>
<attribute name="Implementation-Title" value="Pie Chart"/>
<attribute name="Implementation-Version" value="0.2"/>
</manifest>
<zipfileset src="libs/batik-all.jar" includes="**/*.class" />
<zipfileset src="libs/scenario-0.6x.jar" includes="**/*.class" />
</jar>
</target>

<target name="pack-src">
<zip destfile="dist/piechart_0.2_src.zip">
<zipfileset dir="." includes="libs/**" />
<zipfileset dir="." includes="makepiechart.xml" />
<zipfileset dir="." includes="src_1_5/**/piechart/*.java" />
</zip>
</target>
</project>

 
 
 
 
Comments:

hi , i read this artical for pie chart and downloaded source code but could not able to run it .could u send me information.or give me ur gmail or yahoo id so that i can talk with you thanks in advance ....

Posted by roshan on august 25, 2008 at 08:09 AM CEST #

Hi Roshan. I would prefer if we keeped the questions about your problem in the comments - this way other can hopefully learn by reading the comments. - Morten

Posted by Morten Nobel-Joergensen on august 25, 2008 at 10:18 PM CEST #

Post a Comment:
  • HTML Syntax: NOT allowed
 

« januar 2009
mationtofr
   
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
 
       
Today

AddThis Feed Button

 
© Morten Nobel