ChartCat - Java Chart Library

Tutorial

This tutorial is a quick introduction to ChartCat the "Java Chart Library". It contains a lot of examples that illustrate the basic concepts of the architecture. To run the examples, you need a Java 2 runtime environment (see java.sun.com) and at least the ChartCat Evaluation Version (see http://www.netcat.li).
The full source code of all examples is located in the /src folder of the example package, and the /bin folder contains the compiled versions of it.

ChartCat can perfectly be integrated into the Java Printing Library (ReportCat). If you want to print a chart or if you want to define a report with tables and charts, ReportCat is the perfect add-on.

Please refer to the API documentation for a more detailed description of all members of the API. Before you start with the tutorial, have a quick look at the overview document. It gives you an idea of the functionality of ChartCat.

Download Free Evaluation Version

Table of Contents

Package Structure
Architecture
The First Example
How to Layout Parts
A Bar Chart Example
Adding Tooltips to a Chart
The Part Collection
Adding 3D Perspective to any Chart
Grids and Scales
Pen and PenBox
How to Tag Charts
Printing Charts
Configuring the Look of a Chart
DataModels
Exporting the Chart as PNG
Iterating Over all Elements of a Chart

Further Links

Overview of ChartCat
API documentation
Download evaluation version, Pricing, Purchase
Java Report Printing Library (ReportCat)
NetCat Inc.
Feedback

Package Structure

li.netcat.chart The chart package contains the basic interfaces and classes that define the rough architecture. It contains no discreet implementations. All other packages base on this one.
li.netcat.chart.data The data package contains further data models and adapters that provide data in an appropriate form for example for a legend or a scale. Only advanced users have to deal with this package.
li.netcat.chart.util Contains the building blocks (parts) like graphs, scales, legends and grids that can be used to compose a chart. By convention, the names of all those parts end with "Part".
This package also contains other utilities that help building custom parts.
li.netcat.chart.util.elem This package contains classes and interfaces that make up the basic elements of a chart. Only advanced users have to deal with this package.
li.netcat.chart.util.tag The tag package contains classes and interfaces that make up the tag concept. It proposes a strategy of how to label a graph and how to mange overlaps of tags. Only advanced users have to deal with this package.

Architecture

Because charts often need to be painted off-screen (printing, creating images for the web), ChartCat has a screen independent architecture, which means that it does not depend on the java.awt.Component. An adapter (ChartPanel to be specific) is used to display the chart on the screen. Other adapters can be used to direct the chart to another output channel.

Charts must also be very flexible in its layout and look. Therefore, a "building block" architecture has been chosen. To define a chart, use building blocks like a grid, a graph and/or a scale, plug them together and put them into a chart. The examples of this tutorial will show this process in detail.

The following diagram shows the context of a chart:

ChartContext.gif (4662 bytes)

The building blocks simply have to implement the Part interface to be a first class citizen. So it is very easy to implement and integrate additional building blocks that can coexist with the existing ones.

The First Example

Enough theory for the moment. Here comes the first example.

Example.gif (212 bytes) HelloWorldExample

import java.awt.*;
import javax.swing.*;
import li.netcat.chart.*;
import li.netcat.chart.util.*;

public class HelloWorldExample {
  
  public static void main(String[] args) {
    LabelPart labelPart = new LabelPart("Hello World!");
    labelPart.setAngleDegrees(90);
    open(new ChartPanel(new Chart(labelPart)));
  }
  
  public static void open(JComponent component) {
    JFrame frame = new JFrame();
    frame.setContentPane(component);
    frame.pack();
    frame.setVisible(true);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.toFront();
  }
}

This code opens the following frame:

HelloWorld.gif (1281 bytes)

As you can see in the code, a label part has been allocated and configured. This Part gets packed into a Chart which gets packed into a ChartPanel. Now, we are on the level of a JComponent which can be displayed on the screen.

How to Layout Parts

Because the architecture of ChartCat is completely independent of Java AWT components, the layout managers of AWT cannot be applied to charts. The layout of charts has anyway another layout paradigm than screen components. Screen components are often layouted alongside whereas the parts of a chart are mostly layouted on top of each other. For that reason, an own layout concept has been developed.

There are only two classes involved in the layout of parts: The PanelPart and the PanelPartConstraints.

As you can guess, the PanelPart is a counterpart of the java.awt.Container.  It is an implementation of a Part that can contain other parts. In contrast to AWT, the panel part does not have a pluggable layout manager; the layout manager is directly built in. The PanelPartConstraints describe the rules of how to position a part in the panel.

The following example shows, how to layout the parts in a PanelPart. Consider the following arrangement of parts and its rules:

PanelPart.gif (1135 bytes)

Part Horizontal position Vertical position
A somewhere in the universe somewhere in the universe
B left of B = right of A center of B = bottom of A
C left of C = right of B + 2 pixel center of C = center of (A+B)
D center of D = center of A top of D = bottom of (A+B+C)
E center of E = center of B center of E = center of D
F center of F = 1/3 between left and right of A top of F = top of A

This non trivial example can be layouted using only one PanelPart:

Example.gif (212 bytes) LayoutExample

public class LayoutExample {
  
  public static void main(String[] args) {
    FrameHelper.open(createChart());
  }
  
  public static Chart createChart() {
    PanelPart panel = new PanelPart();
    PanelPartConstraints c = PanelPartConstraints.DEFAULT;
    Part partA = createPart("A", 35);
    Part partB = createPart("B", 10);
    Part partC = createPart("C", 15);
    Part partD = createPart("D", 20);
    Part partE = createPart("E", 30);
    Part partF = createPart("F", 5);
    panel.addPart(partA);
    panel.addPart(partB, c.anchorSouthEast().targetWest());
    panel.addPart(partC, c.anchorEast().targetWest().shiftX(2));
    panel.addPart(partD, c.anchorSouth().targetNorth().referenceX(partA));
    panel.addPart(partE, c.anchorCenter().targetCenter().referenceX(partB).referenceY(partD));
    panel.addPart(partF, c.anchor(1.0f/3.0f, 0.0f).targetNorth().reference(partA));
    return new Chart(panel);
  }
  
  public static Part createPart(String text, int inset) {
    BorderPart borderPart = new BorderPart(new LabelPart(text));
    borderPart.setInsets(inset, inset, inset, inset);
    return borderPart;
  }
}

Most of the parts know themselves, where to position in the universe. For example a grid and a graph that must lie exactly on top of each other know where to position themselves. Simply avoid the constraints object for this sort of parts.

As you could see in this example, the layout mechanism of one single PanelPart is clearly arranged and very powerful. With the help of boxed panel parts, the most complex layouts are possible.

A Bar Chart Example

Since we now know how to layout the parts of a chart, we are ready for the real stuff. Consider the following bar chart and the code that generated this chart:

BarChartExample.gif (4349 bytes)

Example.gif (212 bytes) BarChartExample

public class BarChartExample {
  
  public static void main(String[] args)	{
    ChartModel model = new SimpleChartModel(4, 3, "Bar Chart Example", "x Axis", "y Axis");
    FrameHelper.open(new BarChart(model));
  }

  public static class BarChart extends Chart {
    
    public BarChart(ChartModel chartModel) {
      BarPart barPart = new BarPart(chartModel);
      CartesianGridPart grid = new CartesianGridPart(barPart);
      CartesianScalePart scaleX = new CartesianScalePart(barPart, ChartConstants.BOTTOM);
      CartesianScalePart scaleY = new CartesianScalePart(barPart, ChartConstants.LEFT);
      AreaLegendPart legend = new AreaLegendPart(barPart);
      LabelPart title = new LabelPart(chartModel, "title");
      title.setTextLook(TextLook.getDefaultLarge());
      LabelPart xAxis =  new LabelPart(chartModel, "x");
      xAxis.setTextLook(TextLook.getDefaultMedium());
      LabelPart yAxis =  new LabelPart(chartModel, "y");
      yAxis.setTextLook(TextLook.getDefaultMedium());
      yAxis.setAngleDegrees(90);
      PanelPart panel = new PanelPart();
      PanelPartConstraints c = PanelPartConstraints.DEFAULT;
      panel.addPart(grid);
      panel.addPart(barPart);
      panel.addPart(scaleX);
      panel.addPart(scaleY);
      BorderPart borderPart = new BorderPart(legend);
      panel.addPart(borderPart, c.reference(grid).anchorEast().targetWest().shiftX(10));
      panel.addPart(xAxis, c.referenceX(grid).anchorSouth().targetNorth().shiftY(5));
      panel.addPart(yAxis, c.referenceY(grid).anchorWest().targetEast().shiftX(-5));
      panel.addPart(title, c.anchorNorth().targetSouth().shiftY(-10));
      setContent(panel);
    }
  }
}

Let's have a quick look at the inner class BarChart first. As you can see, this class extends the Chart class and encapsulates the whole layout of a bar chart. It basically allocates a new bar part, a new Cartesian grid, two scales (x and y), and three label parts (title and names of axis). It then takes a panel part and adds all parts to it using suitable layout constraints (see previous chapter). Finally it sets the content to the super class.

The data values for all these parts are provided by the ChartModel. This interface has the following definition:

public interface ChartModel {
  
  public int getRowCount();
  
  public int getColumnCount();
  
  public Object getValueAt(int rowIndex, int columnIndex);

  public String getTextAt(int rowIndex, int columnIndex);
  
  public String getTextForRow(int rowIndex);

  public String getTextForColumn(int columnIndex);
  
  public String getTextForKey(String key);
  
  public void addChartModelListener(ChartModelListener listener);
  
  public void removeChartModelListener(ChartModelListener listener);
  
}

It provides the data in the form of a matrix that has a certain number of rows and columns. Each cell of the matrix has a value and a text. The value might be any object, but is mostly an extension of the class java.lang.Number.

The text of a cell provides the formatted value of the cell and is used to label the chart values. Each row and each column has also a text that is used for the scale or the legend.

The title and other arbitrary text elements of the chart retrieve their text also from the chart model. They use the method getTextForKey(String) as their data source. In the previous example, the title label part uses the key "title" to retrieve the data. There are no predefined values for the keys; the programmer is free to choose his preferred naming for these keys.

Last but not least, chart model listeners can be added to the ChartModel. They get notified whenever a change in the chart model occurs.

Here is the code of the chart model that has been used in the previous example:

public class SimpleChartModel extends AbstractChartModel {
  private Map _texts = new HashMap();
  private int _rowCount;
  private int _columnCount;

  //// constructors
  
  public SimpleChartModel(int rowCount, int columnCount, String title, String x, String y) {
    this(rowCount, columnCount);
    setText("title", title);
    setText("x", x);
    setText("y", y);
  }

  public SimpleChartModel(int rowCount, int columnCount) {
    _rowCount = rowCount;
    _columnCount = columnCount;
  }
  
  //// interface ChartModel
  
  public int getRowCount() {
    return _rowCount;
  }
  
  public int getColumnCount() {
    return _columnCount;
  }
  
  public Object getValueAt(int rowIndex, int columnIndex) {
    return new Integer((1+rowIndex) * (1+columnIndex)); 
  }
  
  public String getTextForKey(String key) {
    return (String)_texts.get(key);
  }
  
  //// private methods
  
  private void setText(String key, String text) {
    _texts.put(key, text);
  }
}

The AbstractChartModel can be extended to define a custom chart model. It manages the list of listeners and offers a method to fire a model change event.

Adding Tooltips to a Chart

Important features of interactive charts are tool tip texts and drilldowns. A tool tip text is a small popup control that appears when the mouse cursor stays over a component. The drilldown mechanism at the other hand enables a user to click into a chart component and open a more detailed view of the selected data. It must also be possible to change the look of the mouse cursor if it is located over a drill down enabled control.
More general, the programmer wants to know what data are currently under the mouse cursor. He gets this information with a call to the method ChartPanel.getLogicalPoint(Point p). With the returned information, the programmer is able to display the tool tip of his choice or to implement a drilldown behavior. The following code shows the idea:

Example.gif (212 bytes) ToolTipExample

First of all, create a chart panel as usual. Then install your tool tip handler with the chart panel and open the chart panel as usual:

  ...
  ChartPanel chartPanel = new ChartPanel(new BarChart(model));
  new MyTooltipHandler(chartPanel).install();
  FrameHelper.open(new JScrollPane(chartPanel));
  ...

And here comes one possible implementations of your tooltip handler:

  private class MyTooltipHandler extends MouseMotionAdapter {
    private ChartPanel _chartPanel;
    
    //// constructor
    
    public MyTooltipHandler(ChartPanel chartPanel) {
      _chartPanel = chartPanel;
    }
    
    //// public methods
    
    public void install() {
      ToolTipManager.sharedInstance().registerComponent(_chartPanel);
      _chartPanel.addMouseMotionListener(this);
    }
    
    public void uninstall() {
      ToolTipManager.sharedInstance().unregisterComponent(_chartPanel);
      _chartPanel.removeMouseMotionListener(this);
    }
    
    //// overridden methods
    
    public void mouseMoved(MouseEvent e) {
      LogicalPoint lp = _chartPanel.getLogicalPoint(e.getPoint());
      String text = null;
      if (lp != null) {
        ChartModel model = lp.getChartModel();
        if (model != null) {
          // use lp and model to calculate the tooltip text
          int col = lp.getColumnIndex();
          int row = lp.getRowIndex();
          if (row >= 0) {
            if (col >= 0) {
              text = model.getTextAt(row, col);
            }
            else {
              text = model.getTextForRow(row);
            }
          }
          else {
            if (col >= 0) {
              text = model.getTextForColumn(col);
            }
          }
        }
      }
      _chartPanel.setToolTipText(text);
    }
  }

The install method registers the chart panel with the tool tip manager and adds a mouse motion listener to the chart panel. The mouseMoved(MouseEvent) method is automatically called whenever the mouse moves over the component. A LogicalPoint is calculated upon each mouse move which gives detailed information about the underlying data structure (chart model, indices etc.). This information is used to construct the corresponding tool tip text. It can also be used to change the look of the mouse cursor or to implement a drilldown behavior.

The Part Collection

The package li.netcat.chart.util contains - beside other utilities - a collection of parts that can be used to build charts. See the package description for an overview. The following diagram shows the class hierarchy of all parts in the collection. Dashed boxes refer to abstract classes.

PartHierarchy.gif (7836 bytes)

By convention, all implementations of the Part interface end with the term "Part". You are encouraged to use the same naming convention if you define your own parts. To define your own part, it is possible to extend an existing part from the collection so that you profit from the existing functionality.

The following example makes use of different parts from the collection:

Collection.gif (28210 bytes)

And here comes the code that produced these charts:

Example.gif (212 bytes) CollectionExample

public class CollectionExample {
  
  public static void main(String[] args) {
    FrameHelper.open(createCharts(), -1, 3);
  }
  
  public static Chart[] createCharts() {
    ChartModel model = new StudentsChartModel();
    return new Chart[] {
        new BarChart(model, ChartConstants.STACK_TYPE_NOT_STACKED, ChartConstants.HORIZONTAL),
        new BarChart(model, ChartConstants.STACK_TYPE_STACKED, ChartConstants.HORIZONTAL),
        new BarChart(model, ChartConstants.STACK_TYPE_STACKED_PERCENT, ChartConstants.HORIZONTAL),
        new BarChart(model, ChartConstants.STACK_TYPE_NOT_STACKED, ChartConstants.VERTICAL),
        new BarChart(model, ChartConstants.STACK_TYPE_STACKED, ChartConstants.VERTICAL),
        new BarChart(model, ChartConstants.STACK_TYPE_STACKED_PERCENT, ChartConstants.VERTICAL),
        new AreaChart(model, ChartConstants.STACK_TYPE_NOT_STACKED, ChartConstants.HORIZONTAL),
        new AreaChart(model, ChartConstants.STACK_TYPE_STACKED, ChartConstants.HORIZONTAL),
        new AreaChart(model, ChartConstants.STACK_TYPE_STACKED_PERCENT, ChartConstants.HORIZONTAL),
        new AreaChart(model, ChartConstants.STACK_TYPE_NOT_STACKED, ChartConstants.VERTICAL),
        new PieChart(model),
        new RingChart(model),
        new LineChart(model, ChartConstants.STACK_TYPE_NOT_STACKED, ChartConstants.HORIZONTAL, true, false),  
        new LineChart(model, ChartConstants.STACK_TYPE_STACKED, ChartConstants.VERTICAL, false, true),
        new RadarChart(model, ChartConstants.STACK_TYPE_NOT_STACKED),
        new RadarChart(model, ChartConstants.STACK_TYPE_STACKED),
        new RadarLineChart(model, ChartConstants.STACK_TYPE_NOT_STACKED, true, true),
        new RadarLineChart(model, ChartConstants.STACK_TYPE_STACKED, true, true),
    };
  }
  
  
  public static class BarChart extends CartesianChart {
    public BarChart(ChartModel model, int type, int orientation) {
      super(new BarPart(model, type, orientation));
    }
  }
  
  public static class AreaChart extends CartesianChart {
    public AreaChart(ChartModel model, int type, int orientation) {
      super(new AreaPart(model, type, orientation)); 
    }
  }
  
  public static class LineChart extends CartesianChart {
    public LineChart(ChartModel model, int type, int orientation, boolean paintLines, boolean paintSymbols) {
      super(new LinePart(model, type, orientation, paintLines, paintSymbols));
    }
  }
  
  public static class RadarChart extends PolarChart {
    public RadarChart(ChartModel model, int type) {
      super(new RadarPart(model, type));
    }
  }
  
  public static class RadarLineChart extends PolarChart {
    public RadarLineChart(ChartModel model, int type, boolean paintLines, boolean paintSymbols) {
      super(new RadarLinePart(model, type, paintLines, paintSymbols));
    }
  }

  public static class PieChart extends Chart {
    public PieChart(ChartModel model) {
      RingPart ringPart = new RingPart(new SumRowsChartModel(model), ChartConstants.STACK_TYPE_STACKED_PERCENT);
      ringPart.setSize(48, 0);
      ringPart.setRadialMove(0, 1, 10.0f);
      PanelPart panel = new PanelPart();
      panel.addPart(ringPart);
      panel.addPart(new PolarInscribePart(ringPart));
      setContent(panel);
    }
  }
  
  public static class RingChart extends Chart {
    public RingChart(ChartModel model) {
      RingPart ringPart = new RingPart(model, ChartConstants.STACK_TYPE_STACKED_PERCENT);
      ringPart.setSize(35, 20);
      ringPart.setRadialMove(3, 1, 10.0f);
      CartesianScalePart cartesianScalePart = new CartesianScalePart(ringPart, ChartConstants.LEFT);
      cartesianScalePart.setAdditionalLineLength(ringPart.getRadius()+5);
      cartesianScalePart.setTagManager(new DefaultTagManager(DefaultTagManager.STYLE_UNDERLINE, DefaultTagManager.METHOD_MOVE));
      cartesianScalePart.setPen(Pen.getDefaultCapButt());
      PanelPart panel = new PanelPart();
      panel.addPart(ringPart);
      panel.addPart(cartesianScalePart);
      setContent(panel);
    }
  }
  
  public static class CartesianChart extends Chart {
    public CartesianChart(CartesianGraphPart cartesianGraphPart) {
      cartesianGraphPart.setSize(140, 100);
      CartesianGridPart cartesianGridPart = new CartesianGridPart(cartesianGraphPart);
      if (cartesianGraphPart.getOrientation() == CartesianGraphPart.VERTICAL) {
        cartesianGridPart.flipFlags();
      }
      PanelPart panel = new PanelPart();
      panel.addPart(cartesianGridPart);
      panel.addPart(cartesianGraphPart);
      panel.addPart(new CartesianScalePart(cartesianGraphPart, CartesianScalePart.LEFT));
      panel.addPart(new CartesianScalePart(cartesianGraphPart, CartesianScalePart.BOTTOM));
      setContent(panel);
    }
  }
  
  public static class PolarChart extends Chart {
    public PolarChart(PolarGraphPart polarGraphPart) {
      polarGraphPart.setSize(48, 0);
      PanelPart panel = new PanelPart();
      panel.addPart(new PolarGridPart(polarGraphPart));
      panel.addPart(polarGraphPart);
      panel.addPart(new PolarScalePart(polarGraphPart));
      setContent(panel);
    }
  }
}

The open method of the class FrameHelper wraps a ChartPanel around each chart and opens them in a JFrame:

  public static void open(Chart[] charts, int rows, int cols) {
    JPanel panel = new JPanel(new GridLayout(rows, cols, 10, 10));
    panel.setBackground(Color.white);
    for (int i=0; i<charts.length; i++) {
      panel.add(new ChartPanel(charts[i]));
    }
    JFrame frame = new JFrame("ChartCat Example");
    frame.setContentPane(new JScrollPane(panel));
    frame.pack();
    frame.setVisible(true);
  }

The two classes CartesianChart and PolarChart have been added to reduce the size of the code. In real life charts, a direct extension of the Chart class would be more adequate. Feel free to define charts according to your needs.

As you can see, the Cartesian parts can have a vertical or horizontal orientation. Furthermore, all charts support stacked values and stacked values that are completed to 100%.

Adding 3D Perspective to any Chart

The implementation of a 3D effect is separated from the implementation of a Part. See the following example that shows, how you can add a 3D effect to an arbitrary part. The following code is the standard way to define a simple bar chart with a grid:

Example.gif (212 bytes) PerspectiveExample

public static class BarChart extends Chart {
    
  public BarChart(ChartModel chartModel) {
    BarPart barPart = new BarPart(chartModel);
    barPart.setSize(200, 150);
    CartesianGridPart grid = new CartesianGridPart(barPart);
    PanelPart panel = new PanelPart();
    panel.addPart(grid);
    panel.addPart(barPart);
    setContent(panel);
  }
}

Perspective1.gif (2183 bytes)

To add a 3D effect, simply wrap a PerspectivePart around the panel as the following example shows:

public static class BarChart extends Chart {
    
  public BarChart(ChartModel chartModel) {
    BarPart barPart = new BarPart(chartModel);
    barPart.setSize(200, 150);
    CartesianGridPart grid = new CartesianGridPart(barPart);
    PanelPart panel = new PanelPart();
    panel.addPart(grid);
    panel.addPart(barPart);
    setContent(new PerspectivePart(panel));
  }
}

Perspective2.gif (3246 bytes)

Use the properties of the PerspectivePart to change the vanishing point or the direction and brightness of the light source and use the GraphLook property of the BarPart to change the color and the outline of the data values:

public static class BarChart extends Chart {
    
  public BarChart(ChartModel chartModel) {
    BarPart barPart = new BarPart(chartModel);
    barPart.setSize(200, 150);
    barPart.getGraphLook().setAreaChartPens(Pen.createFillPens(120, 255));
    CartesianGridPart grid = new CartesianGridPart(barPart);
    PanelPart panel = new PanelPart();
    panel.addPart(grid);
    panel.addPart(barPart);
    PerspectivePart perspectivePart = new PerspectivePart(panel);
    perspectivePart.setVanishingPoint(20, -10);
    perspectivePart.setLight(50, 50);
    setContent(perspectivePart);
  }
}

Perspective3.gif (3050 bytes)

The following code produces a 3D pie chart.  Note the stretch property of the pie that makes an ellipse out of the circle. Another property worth mentioning is the flatness parameter. This is the maximum distance that the line segments used to approximate the curved segments are allowed. Smaller values produce better results at the cost of performance.

Example.gif (212 bytes) PieChartExample3D

public static class PieChart extends Chart {
    
  public PieChart(ChartModel chartModel) {
    RingPart pie = new RingPart(chartModel);
    PerspectivePart pp = new PerspectivePart(pie);
    pie.setSize(46, 0);
    pie.setStretch(3.0f);
    pie.setFlatness(0.01f);
    pie.getGraphLook().setAreaChartPens(Pen.createFillPens(90, 255));
    pp.setVanishingPoint(0, 30);
    pp.setLight(170, 40);
    setContent(pp);
  }
}

PieChart3D.gif (3766 bytes)

How does the PerspectivePart know, how to paint the elements of a part? Well, each element of a part has a perspective property that can be set to FOREGROUND, PERSPECTIVE or BACKGROUND. Let's try it:

public static class BarChart extends Chart {
      
  public BarChart(ChartModel chartModel) {
    BarPart barPart = new BarPart(chartModel);
    barPart.setSize(200, 150);
    barPart.getGraphLook().setAreaChartPens(Pen.createFillPens(120, 255));
    CartesianGridPart grid = new CartesianGridPart(barPart);
    grid.setHorizontalLinesPerspective(ChartConstants.PERSPECTIVE);
    PanelPart panel = new PanelPart();
    panel.addPart(barPart);
    panel.addPart(grid);
    PerspectivePart perspectivePart = new PerspectivePart(panel);
    perspectivePart.setVanishingPoint(20, -10);
    perspectivePart.setLight(50, 50);
    setContent(perspectivePart);
  }
}

Perspective4.gif (3281 bytes)

The PerspectivePart even works together with the legend part as the following example shows:

public static class Legend extends Chart {
    
  public Legend(ChartModel model) {
    // in normal cases, this graph part is also added to the chart.
    BarPart dummy = new BarPart(model);
    
    AreaLegendPart areaLegend = new AreaLegendPart(dummy);
    areaLegend.setPerspective(ChartConstants.PERSPECTIVE);
    areaLegend.setItemTextGap(10);
    PerspectivePart persAreaLegend = new PerspectivePart(areaLegend);
    persAreaLegend.setVanishingPoint(4, -4);
    
    LineLegendPart lineLegend = new LineLegendPart(dummy);
    lineLegend.setPerspective(ChartConstants.PERSPECTIVE);
    lineLegend.setItemTextGap(8);
    PerspectivePart persLineLegend = new PerspectivePart(lineLegend);
    persLineLegend.setVanishingPoint(4, -4);
    persLineLegend.setLight(-10, 20);
    
    PanelPart panel = new PanelPart();
    PanelPartConstraints c = PanelPartConstraints.DEFAULT;
    panel.addPart(persAreaLegend);
    panel.addPart(persLineLegend, c.anchorSouthWest().targetNorthWest().shiftY(1));
    setContent(new BorderPart(panel));
  }
}

Perspective5.gif (1125 bytes)

Since the implementation of the perspective is completely separated from the part, it is also possible to implement an other visual effect like a "ShadowPart" that can decorate all existing parts without any change! As you can see, there are plenty of possibilities to play with the look of a chart. Please see the images in the Overview document and the source code of the perspective example (included in the distribution) for more samples.

Grids and Scales

As you can see in the part hierarchy, there are two implementations for grids: The CartesianGridPart which is used "behind" CartesianGraphParts and the PolarGridPart which is used "behind" PolarGraphParts. The correct positioning of the grids behind the graphs is done automatically. Grids have may porperties and flags that define the apperance of the grid. For exampe, the number of displayed grid lines can be reduced using the methods

PolarGridPart.setRingInterval(int);
PolarGridPart.setSpokeInterval(int);
CartesianGridPart.setGridIntervalX(int);
CartesianGridPart.setGridIntervalY(int);

What about scales? There are two implementation of scales in the library: The CartesianScalePart is used as a horizontal and vertical scale in combination with a Cartesian graph or as a vertical scale in a polar graph. The PolarScalePart is used as a "round" scale for polar graphs. The positioning is also automatic but can be influenced by certain properties (additional line lenght, insets or layout constraints). The number of displayed marks can also be reduced using the methods

PolarScalePart.setMarkInterval(int);
CartesianScalePart.setMarkInterval(int);

The following example shows the function y = x*x for the values x = 0..100. Note that the vertical scale is inverted and that the grid does not paint all lines for the value x. You will find the source code in the example package.

Example.gif (212 bytes) GridIntervalExample

GridExample.gif (6334 bytes)

Pen and PenBox

There are different ways to change the look of a chart. As you know, the chart consists of one or more parts. All parts have a variety of properties that influence the look of the chart. One of the most important properties is the Pen. It basically has three properties:

The pen and all of its properties can be null, which means "completely transparent".

A graph consists of a series of data and each data element probably gets painted with a different pen. The different pens for the data series are provided by the GraphLook class. This property defines the whole look of a graph, i.e. the pens for area charts and the pens and reference symbols for line charts. The following command changes the pens of the areas of a graph to a custom black/white texture:

Example.gif (212 bytes) TextureExample

...
// create own graph look
GraphLook graphLook = new GraphLook();
graphLook.setAreaChartPens(createAreaChartPens());
// set new graph look to chart.
areaPart.setGraphLook(graphLook);
...

This command influences also the legend part, because the legend gets its graph look from the area part:

 

TextureExample.gif (4327 bytes)

Sometimes, not only the graph itself needs its custom look, but also the scales and grids. This can simply be done by setting the relevant pen to the part:

grid.setBackgroundPen(myPen);

The default grid and scale implementations give you even the possibility to influence the color and shape of each single scale element, each grid line and each single background segment. This is done with the concept of a PenBox. The PenBox is a simple pen collection that provides different instances of pens:

grid.setBackgroundPenBox(myPenBox);

If the pen box is set to null, the default pen of this property is used, oterwise, the pen of the pen box is used. The following example shows the usage of pen boxes:

Example.gif (212 bytes) PenBoxExample

public static class RadarLineChart extends Chart {
        
  public RadarLineChart(ChartModel chartModel) {

    // prepare radar line part
    RadarLinePart radarLinePart = new RadarLinePart(chartModel);
    radarLinePart.setSize(140f, 8f);
    radarLinePart.setPaintSymbols(false);
    // set a static scale model that starts at 10, has marks at 40, 60 and ends at 90
    radarLinePart.setRadialScaleModel(new StaticScaleModel(new double[]{10, 40, 60, 90})); 
    // prepare a own graph look
    GraphLook look = new GraphLook();
    // define the pens that are used to paint the lines of the chart
    look.setLineChartPenBox(new MyLineChartPenBox());
    // set the look to the graph
    radarLinePart.setGraphLook(look);

    // prepare the grid
    PolarGridPart grid = new PolarGridPart(radarLinePart);
    // define different background colors
    grid.setBackgroundPenBox(new MyBackgroundPenBox());
    // define different pens for the ring lines
    grid.setRingPenBox(new MyRingPenBox());
    // define a pen box for the spokes and the polar scale
    PenBox scalePenBox = new MyScalePenBox();
    // paint the spokes with the scale pen box
    grid.setSpokePenBox(scalePenBox);
      
    // prepare a polar scale
    PolarScalePart polarScalePart = new PolarScalePart(radarLinePart);
    // paint the text with the scale pen box
    polarScalePart.setTextPenBox(scalePenBox);
    // paint the lineswith the scale pen box
    polarScalePart.setLinePenBox(scalePenBox);
      
    // add everyting to a panel
    PanelPart panel = new PanelPart();
    panel.addPart(grid);
    panel.addPart(radarLinePart);
    panel.addPart(polarScalePart);
    setContent(panel);
  }
}

The resulting chart looks like this:

PenBoxExample.gif (12988 bytes)

How to Tag Charts

Labeling charts is a troublesome story, especially the handling of overlapping labels. ChartCat solves these problems with its "Tag Concept". It uses the naming "tag" instead of the overloaded name "label". An own package (li.netcat.chart.util.tag) has been created that holds the interfaces and classes making up this concept. Scales as well as inscriptions make use of the tag concept.

The default configurations are such that you usually don't have to bother about the correct layout of tags. The following example shows how to inscribe for example a pie chart:

Example.gif (212 bytes) TagExample

PieChartExample.gif (3906 bytes)

This chart is defined by the following class:

public static class PieChart extends Chart {
    
  public PieChart(ChartModel chartModel) {
    RingPart ringPart = new RingPart(chartModel);
    ringPart.setSize(50, 0);
    PanelPart panel = new PanelPart();
    panel.addPart(ringPart);
    panel.addPart(new PolarInscribePart(ringPart));
    setContent(panel);
  }
}

You just have to add a new PolarInscribePart to the chart. There is also a CartesianInscribePart for Cartesian charts and a DirectInscribePart for both types of charts available.

Now lets dig a little deeper into the tag concept and consider the following two bar charts:

TagExample.gif (3541 bytes)

The scale as well as the inscription can freely be configured in its layout. The following two classes define these two bar charts:

public static class BarChart extends Chart {
          
  public BarChart(ChartModel chartModel) {
    BarPart barPart = new BarPart(chartModel);
    barPart.setSize(100, 75);
    CartesianInscribePart inscribePart = new CartesianInscribePart(barPart);
    DefaultTagManager tagManager = new DefaultTagManager(DefaultTagManager.STYLE_TURN, DefaultTagManager.METHOD_MOVE);
    tagManager.setTurnTextLineGap(0);
    inscribePart.setTagManager(tagManager);
    inscribePart.setPosition(ChartConstants.TOP);
    inscribePart.setTargetPosition(1, -3, 0, 0);
    CartesianScalePart scaleX = new CartesianScalePart(barPart, ChartConstants.BOTTOM);
    scaleX.setTagManager(new DefaultTagManager(DefaultTagManager.STYLE_UNDERLINE, DefaultTagManager.METHOD_MOVE));
    CartesianScalePart scaleY = new CartesianScalePart(barPart, ChartConstants.LEFT);
    scaleY.setTagManager(new DefaultTagManager(DefaultTagManager.STYLE_DASH, DefaultTagManager.METHOD_MOVE));
    CartesianGridPart grid = new CartesianGridPart(barPart);
    grid.setPaintBorderFlags(false, false, false, false);
    grid.setPaintMarksFlags(false, false, false, false);
    grid.setBackgroundPaint(null);
    PanelPart panel = new PanelPart();
    panel.addPart(grid);
    panel.addPart(barPart);
    panel.addPart(inscribePart);
    panel.addPart(scaleX);
    panel.addPart(scaleY);
    setContent(panel);
  }
}
        
public static class BarChart2 extends Chart {
          
  public BarChart2(ChartModel chartModel) {
    BarPart barPart = new BarPart(chartModel);
    barPart.setSize(110, 75);
    DirectInscribePart inscribePart = new DirectInscribePart(barPart);
    BasicTagManager tagManager = new BasicTagManager(ChartConstants.LEFT, ChartConstants.CENTER, 90);
    inscribePart.setTagManager(tagManager);
    inscribePart.setTextLook(TextLook.getDefaultNormal());
    inscribePart.setTargetPosition(0, 5, 0, 0);
    CartesianScalePart scaleX = new CartesianScalePart(barPart, ChartConstants.BOTTOM);
    scaleX.setTagManager(new DefaultTagManager(DefaultTagManager.STYLE_TURN, DefaultTagManager.METHOD_MOVE));
    CartesianScalePart scaleY = new CartesianScalePart(barPart, ChartConstants.LEFT);
    scaleY.setTagManager(new DefaultTagManager(DefaultTagManager.STYLE_UNDERLINE, DefaultTagManager.METHOD_REMOVE));
    CartesianGridPart grid = new CartesianGridPart(barPart);
    grid.setPaintBorderFlags(false, false, false, false);
    grid.setPaintMarksFlags(false, false, false, false);
    grid.setBackgroundPaint(null);
    PanelPart panel = new PanelPart();
    panel.addPart(grid);
    panel.addPart(barPart);
    panel.addPart(inscribePart);
    panel.addPart(scaleX);
    panel.addPart(scaleY);
    setContent(panel);
  }
}

The blue code parts correspond to the labels at the top of the first chart or to the direct labels in the bars respectively. The green and yellow parts configure the x and y scale. 

As you can see in the class hierarchy, scale parts and inscribe parts are all extensions of the TagPart. The tag part itself uses a TagManager that is responsible for the final layout of the tags. The default tag manager implementations that come along with ReportCat offer already a lot of possible strategies to do the layout of tags.

Printing Charts

Since ChartCat is independent of the output device, charts can easily be printed using the standard java print API. Let's have a quick look at the API of a Chart. It contains among others two straight forward methods that are used to paint the chart:

public Dimension getSize(Graphics g);

public void paint(Graphics g, int x, int y);

These two slim methods guarantee an easy integration of ChartCat charts into any graphic environment. Nevertheless, printing is a complex process, since the java print API provides just more or less an instance of a Graphics object that can be used to paint on each page.

Fortunately the technology company NetCat AG provides a product called "Java Report Printing Library (ReportCat)" that makes layouting and printing of complex reports quiet easy. As the following example shows, ChartCat and ReportCat work together in a perfect harmony.

Example.gif (212 bytes) PrintExample

PrintExample.gif (11987 bytes)

To run this example, you will need at least the free trial version of ReportCat. Here is the code that produced this report:

public class PrintExample {
  
  public static void main(String[] args) {
    new PrintPreview(new PagePrint(createPrint(), new MyPageDecorator())).open();
  }
  
  public static Print createPrint() {
    ChartModel chartModel = new TemperatureChartModel();
    TableDataModel tableModel = new ChartModelTableDataModelAdapter(chartModel);
    TablePrint table = new TablePrint(tableModel);
    table.setFitWidth(true);
    table.setCellDrawer(Number.class, table.createDefaultCellDrawer(PrintConstants.RIGHT));
    ChartPrint chart = new ChartPrint(new LineChart(chartModel));
    GridPrint gridPrint = new GridPrint(20, 20);
    GridPrintConstraints c = GridPrintConstraints.DEFAULT.hAlignCenter();
    gridPrint.add(chart, c);
    gridPrint.add(table, c);
    return gridPrint;
  }
}

Let's consider the interesting (bold) parts of this code. The TemperatureChartModel is an implementation of a ChartModel that provides the temperature data. Its implementation may get the data from a database or from any other source. To be honest, this data model uses hard coded values for the sake of simplicity. The point is that the table model is derived from the chart model by using the following adapter:

public class ChartModelTableDataModelAdapter implements TableDataModel {
  private ChartModel _chartModel;
  
  //// constructors
  
  public ChartModelTableDataModelAdapter(ChartModel chartModel) {
    _chartModel = chartModel;
  }
    
  public int getRowCount() {
    return _chartModel.getRowCount();
  }
    
  public int getColumnCount() {
    return _chartModel.getColumnCount() + 1;
  }
    
  public Object getValueAt(int rowIndex, int columnIndex) {
    if (columnIndex == 0) {
      return _chartModel.getTextForRow(rowIndex);
    }
    return _chartModel.getTextAt(rowIndex, columnIndex-1);
  }
    
  public Object getValueTypeAt(int rowIndex, int columnIndex) {
    if (columnIndex == 0) {
      return String.class;
    }
    return Number.class;
  }
    
  public boolean hasHeader() {
    return true;
  }

  public Object getHeaderAt(int columnIndex) {
    if (columnIndex == 0) {
      return "Tag";
    }
    return _chartModel.getTextForColumn(columnIndex-1);
  }
    
  public Object getHeaderTypeAt(int columnIndex) {
    return String.class;
  }
    
  public boolean hasFooter() {
    return false;
  }
    
  public Object getFooterAt(int columnIndex) {
    return null;
  }
    
  public Object getFooterTypeAt(int columnIndex) {
    return null;
  }
}

This class adapts the chart model to a data model. It uses the column names of the chart model as titles of the table and includes the names of the rows as the first column:

Adapter1.gif (8361 bytes)

It is also possible to derive the chart model from the table model. However, in real life applications, the data values are probably available in a third, project specific form. Using two adapters would be a good solution:

Adapter.gif (8565 bytes)

Now, lets have a look how the chart itself gets integrated into the report. ReportCat uses "prints" as building blocks for its reports. It is obvious to use an adapter that converts a part into a print. And this is exactly what the class ChartPrint does:

import java.awt.*;
import li.netcat.chart.*;
import li.netcat.print.*;
import li.netcat.print.util.*;

public class ChartPrint extends PainterPrint {
  private Chart _chart;
  
  public ChartPrint(Chart chart) {
    super(new ChartPainter(chart));
  }
  
  public static class ChartPainter implements Painter {
    private Chart _chart;
    
    public ChartPainter(Chart chart) {
      _chart = chart;
    }
    
    public Dimension getSize(Graphics g) {
      return _chart.getSize(g);
    }
    
    public void paint(Graphics g, int x, int y) {
      _chart.paint(g, x, y);
    }
  }
}

This small adapter is the only necessary bridge between ChartCat and ReportCat. Since ChartCat is completely independent of ReportCat and vice versa, this adapter can not be part of any of both products. It must be integrated into the project code.

Data Models

The data for a scale is provided by the ScaleModel. Each scale model has a length and a number of marks. The length of the scale is equal to its size on the screen. Each mark has a position in the scale, a text and a value. The scale model is also able to calculate the position of an arbitrary value. A scale with discrete, non numeric values like "Monday", "Tuesday", ... ,"Sunday" is free to use values like 0, 1, ..., 6 as numeric representatives.

The NumericScaleModel is an extension of the ScaleModel and uses real numeric values that can be absolute or in percent. It is possible to set a numeric range to this type of scale model. After construction, the numeric scale model is initialized with an instance of DefaultScaleModel.

Another player in the concept is the StackModel. It is used to calculate the values of a stacked chart and provides the rounded minimum and maximum values for the numeric scale. Without this functionality, the scale of a chart would end for example at 23.35 instead of 25.

Each GraphPart has a ChartModel, a LegenModel, a StackModel, a ScaleModel, and a NumericScaleModel. The data flows as shown in the following diagram:

Models.gif (2470 bytes)

All of these five models can be configured or replaced after the construction of a GraphPart. For example, if you want another subdivision of the numeric scale,  just reconfigure the DefaultScaleModel, e.g.:

((DefaultScaleModel)myGraphPart.getNumericScaleModel()).setDivideMap(myDivideMap);

Note that the DefaultScaleModel can also be reverted, so that the smallest number is at the top of a horizontal scale. If you want a logarithmic or any other numeric subdivision, replace the whole numeric scale model with your own implementation, and all tool tips, graphs, grids and scales will display correctly:

myGraphPart.setNumericScaleModel(myComplexScaleModel);

Exporting the Chart

The following example shows how to export a Chart to an image file. Note that this example only runs with Java 1.4 or later because of the png export functionality. The chart gets painted into a buffered image and then exported to a file:

Example.gif (212 bytes) ImageExportExample

  public static void main(String[] args) {
    Chart myChart = new BarChart(new MyChartModel());
    // calculate the size of the chart (that depends on the graphics object)
    BufferedImage probe = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
    Graphics2D g2 = probe.createGraphics();
    Dimension size = myChart.getSize(g2);
    g2.dispose();
    // create the image with the right size. This example uses a transparent image.
    BufferedImage image = new BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_ARGB);
    g2 = image.createGraphics();
    // fill the background with white (only necessary if the image is opaque) 
    // g2.setColor(Color.white);
    // g2.fillRect(0, 0, image.getWidth(), image.getHeight());
    
    // paint the chart to the graphics
    myChart.paint(g2, 0, 0);
    g2.dispose();
    // print available image formats for debug purposes
    System.out.println("Available formats: " + Arrays.asList(ImageIO.getWriterFormatNames()));
    // write the image as a png to a file
    String fileName = "c:\\myimage.png";
    File f = new File(fileName);
    try {
      ImageIO.write(image, "png", f);
      System.out.println("File saved to "+fileName);
    }
    catch (IOException x) {
      x.printStackTrace();
    }
  }

Iterating Over all Elements of a Chart

Let's dig a little deeper into the elements of a chart. Sometimes, it's needed to iterate over all elements of a chart, for example to generate an image map. This can be done using the ElementIterator. This interface works much like the normal iterator concept. It provides methods to check, if there are more elements available. If yes, you can call a method to switch to the next element, if no, the iterator is exhausted and can only be thrown away.

How do I get an ElementIterator? Its provided by the IteratableLayout interface, which is an extension of the Layout interface. So, you have to check if the Layout is also an IteratableLayout and cast it down. In fact, all provided implementations Layout also implement the IteratableLayout interface.

Once you positioned the ElementIterator at a valid element you get access to the current translation of the element and access to the Element itself. The Element again provides methods about the bounds and the corresponding data model. If this information is not sufficient, it is possible to cast the Element down to the concrete implementation. This can be for example an AreaElement or a TextElement which provide information about the shape of the area or the text itself.

OK, this was a lot of theory, let's speak in Java language: The following example creates a small bar chart and iterates over all elements of the chat in detail:

Example.gif (212 bytes) ElementIteratorExample

  public static void main(String[] args) {
    Chart myChart = new BarChart(new MyChartModel());
    // size depends on graphics, create a buffered image as probe
    BufferedImage probe = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
    Graphics2D g2 = probe.createGraphics();
    Layout layout = myChart.getContent().getLayout(g2);
    g2.dispose();
    if (layout instanceof IteratableLayout) {
      // downcast to IteratableLayout
      IteratableLayout itLayout = (IteratableLayout)layout; 
      Rectangle2D.Float bounds = new Rectangle2D.Float();
      AffineTransform tr = new AffineTransform();
      float[] coords = new float[6];
      // iterate over all elements
      for (ElementIterator it = itLayout.getElements(); !it.isDone(); it.next()) {
        Element currentElement = it.currentElement();
        LogicalPoint lp = currentElement.getLogicalPoint();
        if (lp != null) {
          // current element represents some data of the model!
          float tx = it.currentTranslateX();
          float ty = it.currentTranslateY();
          currentElement.getBounds(bounds);
          bounds.x += tx; // move bounds to translation
          bounds.y += ty;
          System.out.print("Row "+lp.getRowIndex()+ ", column "+lp.getColumnIndex()+" covers bounds ");
          System.out.println("["+bounds.x+", "+bounds.y+", "+bounds.width+", "+bounds.height+"] ");
          // get more details
          if (currentElement instanceof AreaElement) {
            Shape shape = ((AreaElement)currentElement).getArea();
            tr.setToTranslation(tx, ty);
            System.out.println("  Details:");
            // get a flat pathiterator that only returns lines, and iterate over the shape
            for (PathIterator pIt = shape.getPathIterator(tr, 2.0); !pIt.isDone(); pIt.next()) {
              int segment = pIt.currentSegment(coords);
              if (segment == PathIterator.SEG_MOVETO) {
                System.out.println("    MoveTo ["+coords[0]+", "+coords[1]+"], ");
              }
              else if (segment == PathIterator.SEG_LINETO) {
                System.out.println("    LineTo ["+coords[0]+", "+coords[1]+"], ");
              }
              else if (segment == PathIterator.SEG_CLOSE) {
                System.out.println("    Close.");
              }
            }
          }
        }
      }
    }
  }

This example produces the following output:

Row -1, column -1 covers bounds [-0.25, -175.25, 300.5, 175.5]
  Details:
    MoveTo [0.0, -175.0],
    LineTo [300.0, -175.0],
    LineTo [300.0, 0.0],
    LineTo [0.0, 0.0],
    LineTo [0.0, -175.0],
    Close.
Row 0, column 0 covers bounds [24.75, -44.0, 50.5, 44.25]
  Details:
    MoveTo [25.0, -43.75],
    LineTo [75.0, -43.75],
    LineTo [75.0, 0.0],
    LineTo [25.0, 0.0],
    Close.
Row 0, column 1 covers bounds [74.75, -87.75, 50.5, 88.0]
  Details:
    MoveTo [75.0, -87.5],
    LineTo [125.0, -87.5],
    LineTo [125.0, 0.0],
    LineTo [75.0, 0.0],
    Close.
Row 1, column 0 covers bounds [174.75, -87.75, 50.5, 88.0]
  Details:
    MoveTo [175.0, -87.5],
    LineTo [225.0, -87.5],
    LineTo [225.0, 0.0],
    LineTo [175.0, 0.0],
    Close.
Row 1, column 1 covers bounds [224.75, -175.25, 50.5, 175.5]
  Details:
    MoveTo [225.0, -175.0],
    LineTo [275.0, -175.0],
    LineTo [275.0, 0.0],
    LineTo [225.0, 0.0],
    Close.

Note that the coordinate 0, 0 is in the lower left corner of the chart.


Copyright © 2005 NetCat AG. All rights reserved. Feedback

Chart,Charting,Charts,Java,Java Chart,Java Charting,Java Charts,Chart Java,Charts Java,3D,3D Chart,3Dchart,Graph,Graphs,Chart Graph,Java Graph,Diagram,Diagrams,Java Diagram,Java Diagrams,Curve,Curves,Bar Chart,Pie Chart,Line Chart,Area Chart,Bar Graph,Pie Graph,Line Graph,Java Diagram,Chart Diagram,Package,Library,Chart Library,Chart Engine,Chart Package,Graph Package,Paint,Support,Graphic,Graphics,Print, Chart,Charting,Charts,Java,Java Chart,Java Charting,Java Charts,Chart Java,Charts Java,3D,3D Chart,3Dchart,Graph,Graphs,Chart Graph,Java Graph,Diagram,Diagrams,Java Diagram,Java Diagrams,Curve,Curves,Bar Chart,Pie Chart,Line Chart,Area Chart,Bar Graph,Pie Graph,Line Graph,Java Diagram,Chart Diagram,Package,Library,Chart Library,Chart Engine,Chart Package,Graph Package,Paint,Support,Graphic,Graphics,Print, Chart,Charting,Charts,Java,Java Chart,Java Charting,Java Charts,Chart Java,Charts Java,3D,3D Chart,3Dchart,Graph,Graphs,Chart Graph,Java Graph,Diagram,Diagrams,Java Diagram,Java Diagrams,Curve,Curves,Bar Chart,Pie Chart,Line Chart,Area Chart,Bar Graph,Pie Graph,Line Graph,Java Diagram,Chart Diagram,Package,Library,Chart Library,Chart Engine,Chart Package,Graph Package,Paint,Support,Graphic,Graphics,Print