Skip category labels
Skip category labels
Hello.
I had a problem with a line chart with so many values (more than 200), so that category labels overlapped, and they could't be distinguished.
I've solved this problem by creating a new class derived from CategoryAxis, and redefining drawCategoryLabels so that some ticks are not drawn.
I wonder if there is any simpler way to do that, and if not, if it could be included in CategoryAxis: just a parameter N that specifies the ticks to be printed, 1 tick every N ticks. For example:
- N = 1 means that all ticks are printed (it would be the default)
- N = 10 means that only 1 tick every 10 ticks are printed
The changes to be performed in drawCategoryLabels are straightforward.
God bless,
Jaime
I had a problem with a line chart with so many values (more than 200), so that category labels overlapped, and they could't be distinguished.
I've solved this problem by creating a new class derived from CategoryAxis, and redefining drawCategoryLabels so that some ticks are not drawn.
I wonder if there is any simpler way to do that, and if not, if it could be included in CategoryAxis: just a parameter N that specifies the ticks to be printed, 1 tick every N ticks. For example:
- N = 1 means that all ticks are printed (it would be the default)
- N = 10 means that only 1 tick every 10 ticks are printed
The changes to be performed in drawCategoryLabels are straightforward.
God bless,
Jaime
Below is the class that derives from CategoryAxis.
NOTE: This class has a drawback, in that it does not display tooltips on the tick labels. This is because drawCategoryLabels method prepares tooltips, but it has not access to CategoryAxis' private member categoryLabelToolTips, and there is no public nor protected getCategoryLabelToolTips() in CategoryAxis.
What I propose is to add a constructor to CategoryAxis that receives the interval parameter, and redefine drawCategoryLabels to add the remarked "if". It would not break existing code (this would be only a new constructor) and the tooltips drawback would dissapear.
Regards,
Jaime
NOTE: This class has a drawback, in that it does not display tooltips on the tick labels. This is because drawCategoryLabels method prepares tooltips, but it has not access to CategoryAxis' private member categoryLabelToolTips, and there is no public nor protected getCategoryLabelToolTips() in CategoryAxis.
Code: Select all
/**
* This class enhances <code>CategoryAxis</code> in that it allows
* to skip some labels to be printed in the category axis.
* However, it does not display tooltips on the labels.
*/
public class CategoryAxisSkipLabels extends CategoryAxis
{
private static final int DEFAULT_INTERVAL = 1;
private int m_interval;
/** Default constructor. */
public CategoryAxisSkipLabels()
{
this(null, DEFAULT_INTERVAL);
}
/**
* Constructs an axis with a label.
* @param label Axis label (may be null).
*/
public CategoryAxisSkipLabels(String label)
{
this(label, DEFAULT_INTERVAL);
}
/**
* Constructs a category axis with a label and an interval.
* @param label Axis label (may be null).
* @param interval This number controls the labels to be printed.
* For instance, if <code>interval = 1</code>, all labels are printed; if
* <code>interval = 10</code>, only one of every 10 labels are printed (first label
* is always printed).
*/
public CategoryAxisSkipLabels(String label, int interval)
{
super(label);
m_interval = interval;
}
/**
* Draws the category labels and returns the updated axis state.
* NOTE: This method redefines the corresponding one in <code>CategoryAxis</code>,
* and is a copy of that, with added control to skip some labels to be printed.
*
* @param g2 the graphics device (<code>null</code> not permitted).
* @param dataArea the area inside the axes (<code>null</code> not
* permitted).
* @param edge the axis location (<code>null</code> not permitted).
* @param state the axis state (<code>null</code> not permitted).
* @param plotState collects information about the plot (<code>null</code>
* permitted).
*
* @return The updated axis state (never <code>null</code>).
*/
protected AxisState drawCategoryLabels(Graphics2D g2, Rectangle2D dataArea,
RectangleEdge edge, AxisState state,
PlotRenderingInfo plotState)
{
if (state == null)
{
throw new IllegalArgumentException("Null 'state' argument.");
}
if (isTickLabelsVisible())
{
g2.setFont(getTickLabelFont());
g2.setPaint(getTickLabelPaint());
List ticks = refreshTicks(g2, state, dataArea, edge);
state.setTicks(ticks);
int categoryIndex = 0;
Iterator iterator = ticks.iterator();
while (iterator.hasNext())
{
CategoryTick tick = (CategoryTick) iterator.next();
g2.setPaint(getTickLabelPaint());
CategoryLabelPosition position = getCategoryLabelPositions()
.getLabelPosition(edge);
double x0 = 0.0;
double x1 = 0.0;
double y0 = 0.0;
double y1 = 0.0;
if (edge == RectangleEdge.TOP)
{
x0 = getCategoryStart(categoryIndex, ticks.size(), dataArea, edge);
x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, edge);
y1 = state.getCursor() - getCategoryLabelPositionOffset();
y0 = y1 - state.getMax();
}
else if (edge == RectangleEdge.BOTTOM)
{
x0 = getCategoryStart(categoryIndex, ticks.size(), dataArea, edge);
x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, edge);
y0 = state.getCursor() + getCategoryLabelPositionOffset();
y1 = y0 + state.getMax();
}
else if (edge == RectangleEdge.LEFT)
{
y0 = getCategoryStart(categoryIndex, ticks.size(), dataArea, edge);
y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, edge);
x1 = state.getCursor() - getCategoryLabelPositionOffset();
x0 = x1 - state.getMax();
}
else if (edge == RectangleEdge.RIGHT)
{
y0 = getCategoryStart(categoryIndex, ticks.size(), dataArea, edge);
y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, edge);
x0 = state.getCursor() + getCategoryLabelPositionOffset();
x1 = x0 - state.getMax();
}
Rectangle2D area = new Rectangle2D.Double(x0, y0, (x1 - x0), (y1 - y0));
Point2D anchorPoint =
RectangleAnchor.coordinates(area, position.getCategoryAnchor());
// THIS CODE IS NOW CONTROLLED BY THE "IF" =============
if (categoryIndex % m_interval == 0)
{
TextBlock block = tick.getLabel();
block.draw(g2, (float) anchorPoint.getX(), (float) anchorPoint.getY(),
position.getLabelAnchor(), (float) anchorPoint.getX(),
(float) anchorPoint.getY(), position.getAngle());
Shape bounds = block.calculateBounds(g2, (float) anchorPoint.getX(),
(float) anchorPoint.getY(),
position.getLabelAnchor(),
(float) anchorPoint.getX(),
(float) anchorPoint.getY(),
position.getAngle());
if (plotState != null)
{
EntityCollection entities = plotState.getOwner().getEntityCollection();
if (entities != null)
{
//String tooltip = (String) categoryLabelToolTips.get(tick.getCategory());
String tooltip = null;
entities.add(new TickLabelEntity(bounds, tooltip, null));
}
}
}
// END IF ========================================
categoryIndex++;
}
if (edge.equals(RectangleEdge.TOP))
{
double h = state.getMax();
state.cursorUp(h);
}
else if (edge.equals(RectangleEdge.BOTTOM))
{
double h = state.getMax();
state.cursorDown(h);
}
else if (edge == RectangleEdge.LEFT)
{
double w = state.getMax();
state.cursorLeft(w);
}
else if (edge == RectangleEdge.RIGHT)
{
double w = state.getMax();
state.cursorRight(w);
}
}
return state;
}
}
Regards,
Jaime
Here is another way with jfc-0.9.21
Code: Select all
// create categories...
final String[] categories = new String[50];
for (int i=0; i<categories.length; i++) {
categories[i] = "Data"+String.valueOf(i);
}
// prepare for skip label categories...
final String[] extendedCategories = new String[ categories.length ];
for( int i=0; i< categories.length; i++ )
{
if( i%2 == 0 )
extendedCategories[i] = categories[i];
else
extendedCategories[i] = " ";
}
........ produce a chart ..............
// customise skip label categories...
final ExtendedCategoryAxis extendedCategoryAxis = new ExtendedCategoryAxis(null);
for (int i=0; i<categories.length; i++) {
extendedCategoryAxis.addSubLabel(categories[i], extendedCategories[i]);
}
Font theFont = extendedCategoryAxis.getTickLabelFont();
extendedCategoryAxis.setTickLabelFont(new Font("Arial", Font.PLAIN, 0));
extendedCategoryAxis.setSubLabelFont(theFont);
plot.setDomainAxis(extendedCategoryAxis);
I would like to try out this solution.. So I copied it down and included it into one of my projects and I can not seem to find RectangleEdge. Can you help me out with that..
I am running JFreeChart 1.0.1
Here are the imports I included into the Code
package chartingTest;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.AxisState;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.Point2D;
import java.awt.Shape;
import org.jfree.chart.plot.PlotRenderingInfo;
import org.jfree.chart.axis.CategoryTick;
import org.jfree.chart.axis.CategoryLabelPosition;
import org.jfree.chart.entity.EntityCollection;
import org.jfree.chart.entity.TickLabelEntity;
import org.jfree.text.TextBlock;
import java.util.List;
import java.util.Iterator;
import java.util.Vector;
I am running JFreeChart 1.0.1
Here are the imports I included into the Code
package chartingTest;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.AxisState;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.Point2D;
import java.awt.Shape;
import org.jfree.chart.plot.PlotRenderingInfo;
import org.jfree.chart.axis.CategoryTick;
import org.jfree.chart.axis.CategoryLabelPosition;
import org.jfree.chart.entity.EntityCollection;
import org.jfree.chart.entity.TickLabelEntity;
import org.jfree.text.TextBlock;
import java.util.List;
import java.util.Iterator;
import java.util.Vector;
of all the things I lost, I miss my mind the most
Thanks I just found it a little while ago.. Not all the classes are in the API doc so that threw me off. I eventually went into the source and there it was..
for the record and to save someone else the effort.. Here are the import statements.
Hey thanks for helping me out

for the record and to save someone else the effort.. Here are the import statements.
Code: Select all
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.ui.RectangleAnchor;
import org.jfree.chart.axis.AxisState;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.Point2D;
import java.awt.Shape;
import org.jfree.ui.RectangleEdge;
import org.jfree.chart.plot.PlotRenderingInfo;
import org.jfree.chart.axis.CategoryTick;
import org.jfree.chart.axis.CategoryLabelPosition;
import org.jfree.chart.entity.EntityCollection;
import org.jfree.chart.entity.TickLabelEntity;
import org.jfree.text.TextBlock;
import java.util.List;
import java.util.Iterator;
import java.util.Vector;

of all the things I lost, I miss my mind the most
Okay.. I have it working.. All you have to do is call
and pass in the interval..
The problem becomes knowing when to set this interval (at what point the labels start to overlap) and what to set it to (how many labels do I have space to actually draw)..
I will figure this out and I will post the answer as soon as I do.. That is not a problem. I am just wondering if anyone else has already solved this issue and if so could they give some guidance, or post it..
A quick look at the super classes seems to yield the following possibilities:
Does anyone have any thoughts on this? Is there a better way to figure out when and what to set this interval to?
Code: Select all
public CategoryAxisSkipLabels(String label, int interval)
The problem becomes knowing when to set this interval (at what point the labels start to overlap) and what to set it to (how many labels do I have space to actually draw)..
I will figure this out and I will post the answer as soon as I do.. That is not a problem. I am just wondering if anyone else has already solved this issue and if so could they give some guidance, or post it..
A quick look at the super classes seems to yield the following possibilities:
Code: Select all
CategoryLabelPosition::getWidthRatio() and Axis::getLabelInsets() or getTickLabelInsets().
of all the things I lost, I miss my mind the most
the final solution.. Seems to work really well..
After spending some time thinking about how to calculate N it came to me that this is futile and not the right solution. I tried various methods of adding up all the labels widths, or lengths, and comparing that to the axis length. Replacing the labels so they are centered. Several approaches. Non were very good, well they weren’t. Then it came to me. Who care about what N is..
Simply do not draw labels which are terminated with dots..
So that is what I did. And it works great. So simple.. I am posting my solution, in its entirety, as promised. If you are in the same boat as me I hope it helps you out.
Thanks to jsaiz for his original post..
ray lukas pilot software
Simply do not draw labels which are terminated with dots..
So that is what I did. And it works great. So simple.. I am posting my solution, in its entirety, as promised. If you are in the same boat as me I hope it helps you out.
Thanks to jsaiz for his original post..
ray lukas pilot software
Code: Select all
package chartingTest;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.ui.RectangleAnchor;
import org.jfree.chart.axis.AxisState;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.Point2D;
import java.awt.Shape;
import org.jfree.ui.RectangleEdge;
import org.jfree.chart.plot.PlotRenderingInfo;
import org.jfree.chart.axis.CategoryTick;
import org.jfree.chart.axis.CategoryLabelPosition;
import org.jfree.chart.entity.EntityCollection;
import org.jfree.chart.entity.TickLabelEntity;
import org.jfree.text.TextBlock;
import org.jfree.text.TextLine;
import java.util.List;
import java.util.Iterator;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* This class enhances <code>CategoryAxis</code> in that it allows
* to skip some labels to be printed in the category axis.
* However, it does not display tooltips on the labels.
*/
public class CategoryAxisSkipLabels extends CategoryAxis {
private static final int DEFAULT_INTERVAL = 1;
private static Pattern overlaidLabelPattern = Pattern.compile(".*[\\.]");
private int m_interval;
/** Default constructor. */
public CategoryAxisSkipLabels() {
this(null, DEFAULT_INTERVAL);
}
/**
* Constructs an axis with a label.
* @param label Axis label (may be null).
*/
public CategoryAxisSkipLabels(String label) {
this(label, DEFAULT_INTERVAL);
}
/**
* Constructs a category axis with a label and an interval.
* @param label Axis label (may be null).
* @param interval This number controls the labels to be printed.
* For instance, if <code>interval = 1</code>, all labels are printed; if
* <code>interval = 10</code>, only one of every 10 labels are printed (first label
* is always printed).
*/
public CategoryAxisSkipLabels(String label, int interval) {
super(label);
m_interval = interval;
}
/*
There are some things to note about this method of skipping
category labels. First a label can be several lines of text.
The existing TextLine only seems to let you grab the first
and last fragment. I am assuming, perhaps incorrectly, that
these fragments represent wrapped lines of the label. This
first fragmentation, last fragmentation, means that I am
currently not able to check the full text of the label. I
intend to explore this some more (or accept feedback from
the JfreeChart Developers).
The second draw back to this approach is that any labels that
legitimately end with a period will never be drawn to matter
how much space there is. There are several approaches to
solving this issue. For us this will never happen. Dates, no
matter what language and script do not end in period. Along
with this code I am also supping to our developers a simple
routine which uses the GREP patter defined in
CategoryAxisSkipLables which tests for all occurrences of period
terminated labels and appends a space onto them. This will mess
up the label tick centering a bit but I am willing to accept that
for now. Perhaps I will look at overriding the TextLine object to
include the GREP pattern and present a drawable flag which
CategoryAxisSkipLables will check.. In my spare time.. HA..
Well I hope that this solution, and or these ideas, is of help
to someone.
*/
private boolean hasBeenOverlaid(TextBlock tickLabel) {
boolean dotsFound = false;
List lines = tickLabel.getLines();
Iterator linesIter = lines.iterator();
while ((linesIter.hasNext()) && (!dotsFound)){
TextLine textLine = (TextLine)linesIter.next();
String firstText = textLine.getFirstTextFragment().getText();
String lastText = textLine.getLastTextFragment().getText();
dotsFound =
(((Matcher)(CategoryAxisSkipLabels.overlaidLabelPattern.matcher(firstText))).matches() &&
((Matcher)(CategoryAxisSkipLabels.overlaidLabelPattern.matcher(lastText))).matches());
}
return dotsFound;
}
/**
* Draws the category labels and returns the updated axis state.
* NOTE: This method redefines the corresponding one in <code>CategoryAxis</code>,
* and is a copy of that, with added control to skip some labels to be printed.
*
* @param g2 the graphics device (<code>null</code> not permitted).
* @param dataArea the area inside the axes (<code>null</code> not
* permitted).
* @param edge the axis location (<code>null</code> not permitted).
* @param state the axis state (<code>null</code> not permitted).
* @param plotState collects information about the plot (<code>null</code>
* permitted).
*
* @return The updated axis state (never <code>null</code>).
*/
protected AxisState drawCategoryLabels(Graphics2D g2, Rectangle2D dataArea,
RectangleEdge edge, AxisState state,
PlotRenderingInfo plotState) {
double maxWidth = 0.0;
double maxHeight = 0.0;
if (state == null) {
throw new IllegalArgumentException("Null 'state' argument.");
}
if (isTickLabelsVisible()) {
g2.setFont(getTickLabelFont());
g2.setPaint(getTickLabelPaint());
List ticks = refreshTicks(g2, state, dataArea, edge);
state.setTicks(ticks);
double totalWidth = 0.0;
int categoryIndex = 0;
Iterator iterator = ticks.iterator();
while (iterator.hasNext()) {
CategoryTick tick = (CategoryTick) iterator.next();
g2.setPaint(getTickLabelPaint());
CategoryLabelPosition position = getCategoryLabelPositions().getLabelPosition(edge);
Rectangle2D area = calculateNewDrawingRegion(categoryIndex, ticks, dataArea, state, edge);
Point2D anchorPoint = RectangleAnchor.coordinates(area, position.getCategoryAnchor());
// THIS CODE IS NOW CONTROLLED BY THE "IF" =============
//if (categoryIndex % m_interval == 0){
if (!hasBeenOverlaid(tick.getLabel())){
TextBlock block = tick.getLabel();
// size of this label
org.jfree.ui.Size2D size = block.calculateDimensions(g2);
block.draw(g2, (float) anchorPoint.getX(), (float) anchorPoint.getY(),
position.getLabelAnchor(), (float) anchorPoint.getX(),
(float) anchorPoint.getY(), position.getAngle());
Shape bounds = block.calculateBounds(g2, (float) anchorPoint.getX(),
(float) anchorPoint.getY(),
position.getLabelAnchor(),
(float) anchorPoint.getX(),
(float) anchorPoint.getY(),
position.getAngle());
if (plotState != null) {
EntityCollection entities = plotState.getOwner().getEntityCollection();
if (entities != null) {
//String tooltip = (String) categoryLabelToolTips.get(tick.getCategory());
String tooltip = null;
entities.add(new TickLabelEntity(bounds, tooltip, null));
}
}
}
// END IF ========================================
categoryIndex++;
}
// shrink to dots and then comapre this with the area and data area..
if (edge.equals(RectangleEdge.TOP)) {
//System.out.println("RectangleEdge.TOP ")
double h = state.getMax();
state.cursorUp(h);
}
else if (edge.equals(RectangleEdge.BOTTOM)){
double h = state.getMax();
state.cursorDown(h);
}
else if (edge == RectangleEdge.LEFT){
double w = state.getMax();
state.cursorLeft(w);
}
else if (edge == RectangleEdge.RIGHT){
double w = state.getMax();
state.cursorRight(w);
}
}
return state;
}
private Rectangle2D calculateNewDrawingRegion(int categoryIndex, List ticks, Rectangle2D dataArea,
AxisState state, RectangleEdge edge) {
double x0 = 0.0;
double x1 = 0.0;
double y0 = 0.0;
double y1 = 0.0;
if (edge == RectangleEdge.TOP) {
x0 = getCategoryStart(categoryIndex, ticks.size(), dataArea, edge);
x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, edge);
y1 = state.getCursor() - getCategoryLabelPositionOffset();
y0 = y1 - state.getMax();
}
else if (edge == RectangleEdge.BOTTOM) {
x0 = getCategoryStart(categoryIndex, ticks.size(), dataArea, edge);
x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, edge);
y0 = state.getCursor() + getCategoryLabelPositionOffset();
y1 = y0 + state.getMax();
}
else if (edge == RectangleEdge.LEFT) {
y0 = getCategoryStart(categoryIndex, ticks.size(), dataArea, edge);
y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, edge);
x1 = state.getCursor() - getCategoryLabelPositionOffset();
x0 = x1 - state.getMax();
}
else if (edge == RectangleEdge.RIGHT) {
y0 = getCategoryStart(categoryIndex, ticks.size(), dataArea, edge);
y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, edge);
x0 = state.getCursor() + getCategoryLabelPositionOffset();
x1 = x0 - state.getMax();
}
return new Rectangle2D.Double(x0, y0, (x1 - x0), (y1 - y0));
}
}
of all the things I lost, I miss my mind the most
small change
when we integrated my changes into our build we found something.
if (plotState != null) {
EntityCollection entities = plotState.getOwner().getEntityCollection();
was from the old CatAxis.. Sometime, I do not know why, the plotState owner will be null and in that case this will generate a null pointer exception. So you will need to test for that and skip this block of code just as you did when plotState was null.. In other words.. Change in my example
this
to this (or something like it)
there you go that should fix the problem. Sorry about that I was trying to get a solution out to the world quickly and ....
ray
if (plotState != null) {
EntityCollection entities = plotState.getOwner().getEntityCollection();
was from the old CatAxis.. Sometime, I do not know why, the plotState owner will be null and in that case this will generate a null pointer exception. So you will need to test for that and skip this block of code just as you did when plotState was null.. In other words.. Change in my example
this
Code: Select all
if (plotState != null)
Code: Select all
if ((plotState != null) && (platState.getOwner() != null)) {
ray
of all the things I lost, I miss my mind the most
Ah, this looks to be exactly what I need, thanks for the code. One question, though... when I implement this, I wind up with the correct number of labels being drawn at the bottom, but JFreeChart seems to be allocating horizontal real estate for each of the N labels (where N is usually 5 or so, out of 30+ data points) as though each data point were going to have a label drawn for it, even though only one out of every 5 or 6 is. Thus, I wind up with half a dozen of "..." labels, with lots of space between them. Any idea why that might be?


So that is why I say JFreeChart does not think that it has room to draw them, even though it visually looks like there is..
See what is happening here.. drawCategoryLabels runs because a draw request was issued buy the windows management system (either a user changed the size of whatever), and says give me the drawing parameters like the font size and all that and then calls refreshTicks() in my superclass (CategoryAxis in this case) and then we say “hey does JfreeChart think that there is enough room to draw this label if so go ahead and draw it. If not skip it”.. But it does look to me like we are skipping more than we need to. I agree.. I, or someone, would have to delve into the refreshTicks() method a little more. I am planning on it but I am pretty swamped at work.. That is a horrible thing to say but.. It is true.
I think that you are right, but I don't know the answer.. Maybe this weekend I will get some time to play some more wiht this.


of all the things I lost, I miss my mind the most
Posted working, tested solution.. Category Skip Labels


Simply go to the Patch section of the JFreeChart page. The id for this post is 1532660 the title is CategoryAxis Skip Labels and Tick Marks. Posted today, August 01, 2006..
I hope this helps you guys out..

now let see where are those crawfish flies...
ray lukas
of all the things I lost, I miss my mind the most