When is log-log to be added

A discussion forum for JFreeChart (a 2D chart library for the Java platform).
Locked
Steven W. Rock

When is log-log to be added

Post by Steven W. Rock » Fri Feb 15, 2002 3:08 pm

Hi All,

Thanks for such a well thought out graphing package. I've examined several and have choosen JFreeChart for a major university research project. The only prolem is that we need to be able to toggle on and off log plotting on the x and/or y axis. When will this capability be ready. If we wanted to implement it ourselves, how long would it take to implement?

Cheers,
Steven Rock
Computer Consultant

David Gilbert

Re: When is log-log to be added

Post by David Gilbert » Fri Feb 15, 2002 3:18 pm

Hi Steven,

Another developer e-mailed me earlier this week to say he was working on a logarithmic axis and hoped to send me some code next week. I'll keep you posted.

I think the log scaling will be pretty straightforward - just change the methods that convert data values <---> Java2D values. The difficult part is getting the tick labels to behave themselves. At least I think that is what will be difficult - until I start coding something, I'm never really sure...

Regards,

DG.

Steven W. Rock

Re: When is log-log to be added

Post by Steven W. Rock » Thu Mar 07, 2002 7:07 pm

Hi David,

Any update on log-log? Do you have a time estimate when this will be added? Can I contact this other developer you mention myself?

Cheers,
Steven Rock
Computer Consultant

David Gilbert

Re: When is log-log to be added

Post by David Gilbert » Fri Mar 08, 2002 11:44 am

Hi Steven,

I'll e-mail the developer and ask. I'll also ask him if it is OK to pass on his e-mail address to you.

Regards,

Dave Gilbert

Mansheung Fung

Re: When is log-log to be added

Post by Mansheung Fung » Fri Mar 08, 2002 6:27 pm

I made some changes - seems to be working for me. Maybe you guys can take a look and make it "better" !

I made changes to classes: TickUnits.java, VerticalNumberAxis.java

TickUnits.java
===========
package com.jrefinery.chart;

import java.util.*;

/**
* A collection of tick units.
*/
public class TickUnits {

/** Storage for the tick units. */
protected List units;

/**
* Constructs a new collection of tick units.
*/
public TickUnits() {
this.units = new ArrayList();
}

/**
* Adds a tick unit to the collection.
* <P>
* The tick units are maintained in ascending order.
*/
public void add(TickUnit unit) {

units.add(unit);
Collections.sort(units);

}

public TickUnit getNextHigherTickUnit(TickUnit unit) {
int index = Collections.binarySearch(units, unit);
if (index>=0) {
return (TickUnit)units.get(Math.min(index + 1, units.size()-1));
}
else {
index = -(index + 1);
return (TickUnit)units.get(Math.min(index, units.size()-1));
}
}

public TickUnit getNextHigherTickUnit(Number value) {
return this.getNextHigherTickUnit(new NumberTickUnit(value, null));
}

public TickUnit getNextLowerTickUnit(TickUnit unit) {
int index = Collections.binarySearch(units, unit);
if (index>=0) {
return (TickUnit)units.get(Math.max(index - 1, 0));
}
else {
index = -(index + 1);
return (TickUnit)units.get(Math.max(index-1, 0));
}
}

public TickUnit getNextLowerTickUnit(Number value) {
return this.getNextLowerTickUnit(new NumberTickUnit(value, null));
}

/**
* Returns the tick unit in the collection that is closest in size to the specified unit.
* @param unit The unit.
* @returns The unit in the collection that is closest in size to the specified unit.
*/
public TickUnit getNearestTickUnit(TickUnit unit) {

int index = Collections.binarySearch(units, unit);
if (index>=0) {
return (TickUnit)units.get(index);
}
else {
index = -(index + 1);
return (TickUnit)units.get(Math.min(index, units.size()));
}

}

/**
* Finds the tick unit that is closest to the specified value.
*/
public TickUnit getNearestTickUnit(Number value) {

return this.getNearestTickUnit(new NumberTickUnit(value, null));

}

}

------------------------------------------------------
VerticalNumberAxis.java
=================

package com.jrefinery.chart;

import java.awt.*;
import java.awt.font.*;
import java.awt.geom.*;
import java.text.*;
import java.util.*;

import com.jrefinery.chart.event.*;

/**
* A standard linear value axis, for values displayed vertically.
* <P>
* Note that bug 4273469 on the Java Developer Connection talks about why the grid lines don't
* always line up with the tick marks precisely.
*
*/
public class VerticalNumberAxis extends NumberAxis implements VerticalAxis {

/** A flag indicating whether or not the axis label is drawn vertically. */
protected boolean labelDrawnVertical;

/** log value */
protected boolean logScale = false;

/**
* Constructs a vertical number axis, using default values where necessary.
*/
public VerticalNumberAxis() {

this(null);

}

/**
* Constructs a vertical number axis, using default values where necessary.
* @param label The axis label (null permitted).
*/
public VerticalNumberAxis(String label) {

this(label,
Axis.DEFAULT_AXIS_LABEL_FONT,
ValueAxis.DEFAULT_MINIMUM_AXIS_VALUE,
ValueAxis.DEFAULT_MAXIMUM_AXIS_VALUE);

this.autoRange = true;

}

/**
* Constructs a vertical number axis.
* @param label The axis label (null permitted).
* @param labelFont The font for displaying the axis label.
* @param minimumAxisValue The lowest value shown on the axis.
* @param maximumAxisValue The highest value shown on the axis.
*/
public VerticalNumberAxis(String label, Font labelFont,
double minimumAxisValue, double maximumAxisValue) {

this(label,
labelFont,
Axis.DEFAULT_AXIS_LABEL_PAINT,
Axis.DEFAULT_AXIS_LABEL_INSETS,
true, // vertical axis label
true, // tick labels visible
Axis.DEFAULT_TICK_LABEL_FONT,
Axis.DEFAULT_TICK_LABEL_PAINT,
Axis.DEFAULT_TICK_LABEL_INSETS,
true, // tick marks visible
Axis.DEFAULT_TICK_STROKE,
true, // auto range
true, // auto range includes zero
NumberAxis.DEFAULT_MINIMUM_AUTO_RANGE,
minimumAxisValue,
maximumAxisValue,
false, // inverted
true, // auto tick unit selection
NumberAxis.DEFAULT_TICK_UNIT,
true, // grid lines visible
ValueAxis.DEFAULT_GRID_LINE_STROKE,
ValueAxis.DEFAULT_GRID_LINE_PAINT,
0.0,
ValueAxis.DEFAULT_CROSSHAIR_STROKE,
ValueAxis.DEFAULT_CROSSHAIR_PAINT);

}

/**
* Constructs a vertical number axis.
* @param label The axis label.
* @param labelFont The font for displaying the axis label.
* @param labelPaint The paint used to draw the axis label.
* @param labelInsets Determines the amount of blank space around the label.
* @param labelDrawnVertical Flag indicating whether or not the label is drawn vertically.
* @param tickLabelsVisible Flag indicating whether or not tick labels are visible.
* @param tickLabelFont The font used to display tick labels.
* @param tickLabelPaint The paint used to draw tick labels.
* @param tickLabelInsets Determines the amount of blank space around tick labels.
* @param showTickMarks Flag indicating whether or not tick marks are visible.
* @param tickMarkStroke The stroke used to draw tick marks (if visible).
* @param autoRange Flag indicating whether or not the axis is automatically scaled to fit the
* data.
* @param autoRangeIncludesZero A flag indicating whether or not zero *must* be displayed on
* axis.
* @param autoRangeMinimum The smallest automatic range allowed.
* @param minimumAxisValue The lowest value shown on the axis.
* @param maximumAxisValue The highest value shown on the axis.
* @param inverted A flag indicating whether the axis is normal or inverted (inverted means
* running from positive to negative).
* @param autoTickUnitSelection A flag indicating whether or not the tick units are
* selected automatically.
* @param tickUnit The tick unit.
* @param showGridLines Flag indicating whether or not grid lines are visible for this axis.
* @param gridStroke The pen/brush used to display grid lines (if visible).
* @param gridPaint The color used to display grid lines (if visible).
* @param crosshairValue The value at which to draw an optional crosshair (null permitted).
* @param crosshairStroke The pen/brush used to draw the crosshair.
* @param crosshairPaint The color used to draw the crosshair.
*/
public VerticalNumberAxis(String label,
Font labelFont, Paint labelPaint, Insets labelInsets,
boolean labelDrawnVertical,
boolean tickLabelsVisible, Font tickLabelFont, Paint tickLabelPaint,
Insets tickLabelInsets,
boolean tickMarksVisible, Stroke tickMarkStroke,
boolean autoRange, boolean autoRangeIncludesZero,
Number autoRangeMinimum,
double minimumAxisValue, double maximumAxisValue,
boolean inverted,
boolean autoTickUnitSelection,
NumberTickUnit tickUnit,
boolean gridLinesVisible, Stroke gridStroke, Paint gridPaint,
double crosshairValue, Stroke crosshairStroke, Paint crosshairPaint) {

super(label,
labelFont, labelPaint, labelInsets,
tickLabelsVisible,
tickLabelFont, tickLabelPaint, tickLabelInsets,
tickMarksVisible,
tickMarkStroke,
autoRange, autoRangeIncludesZero, autoRangeMinimum,
minimumAxisValue, maximumAxisValue,
inverted,
autoTickUnitSelection, tickUnit,
gridLinesVisible, gridStroke, gridPaint,
crosshairValue, crosshairStroke, crosshairPaint);

this.labelDrawnVertical = labelDrawnVertical;

}

public boolean isLogScale() {
return this.logScale;
}

public void setLogScale(boolean flag) {
if(this.logScale != flag) {
this.logScale = flag;
if(this.logScale) {
this.inverted = false;
}
this.notifyListeners(new AxisChangeEvent(this));
}
}

/**
* Returns a flag that indicates whether or not the axis label is drawn with a vertical
* orientation (this saves space).
* @return A flag that indicates whether or not the axis label is drawn with a vertical
* orientation.
*/
public boolean isLabelDrawnVertical() {
return this.labelDrawnVertical;
}

/**
* Sets the flag that controls whether or not the axis label is drawn with a vertical
* orientation.
* @param flag The flag.
*/
public void setLabelDrawnVertical(boolean flag) {

if (this.labelDrawnVertical!=flag) {
this.labelDrawnVertical = flag;
this.notifyListeners(new AxisChangeEvent(this));
}

}

/**
* Configures the axis to work with the specified plot. If the axis has auto-scaling, then sets
* the maximum and minimum values.
*/
public void configure() {
if (isAutoRange()) {
autoAdjustRange();
}
}

// /**
// * Translates the data value to the display coordinates (Java 2D User Space) of the chart.
// * @param dataValue The value to be plotted.
// * @param plotArea The plot area in Java 2D User Space.
// */
// public double translatedValue(Number dataValue, Rectangle2D plotArea) {
//
// return this.translateValueToJava2D(dataValue, plotArea);
//
// }

public double translateValueToJava2D(double value, Rectangle2D plotArea) {

double axisMin = minimumAxisValue;
double axisMax = maximumAxisValue;

double maxY = plotArea.getMaxY();
double minY = plotArea.getMinY();

if (inverted) {
return minY + (((value - axisMin)/(axisMax - axisMin)) * (maxY - minY));
}
else {
if(this.logScale) {
if(value <= 0) {
return maxY;
}
else {
return maxY - ((Math.log(value/axisMin)/Math.log(axisMax/axisMin)) * (maxY - minY));
}
}
else {
return maxY - (((value - axisMin)/(axisMax - axisMin)) * (maxY - minY));
}
}
}

public double translateJava2DtoValue(float java2DValue, Rectangle2D plotArea) {
double axisMin = minimumAxisValue;
double axisMax = maximumAxisValue;
double plotY = plotArea.getY();
double plotMaxY = plotArea.getMaxY();
if (inverted) {
return axisMin + (java2DValue-plotY)/(plotMaxY-plotY)*(axisMax-axisMin);
}
else {
if(this.logScale) {
if(java2DValue <= 0) {
return axisMax;
}
else {
return axisMax - ((Math.log(java2DValue/plotY)/Math.log(plotMaxY/plotY)) * (axisMax - axisMin));
}
}
else {
return axisMax - (java2DValue-plotY)/(plotMaxY-plotY)*(axisMax-axisMin);
}
}
}

/**
* Rescales the axis to ensure that all data is visible.
*/
public void autoAdjustRange() {

if (plot!=null) {
if (plot instanceof VerticalValuePlot) {
VerticalValuePlot vvp = (VerticalValuePlot)plot;

Number u = vvp.getMaximumVerticalDataValue();
double upper = this.DEFAULT_MAXIMUM_AXIS_VALUE;
if (u!=null) {
upper = u.doubleValue();
}

Number l = vvp.getMinimumVerticalDataValue();
double lower = this.DEFAULT_MINIMUM_AXIS_VALUE;
if (l!=null) {
lower = l.doubleValue();
}

double range = upper-lower;

// ensure the autorange is at least <minRange> in size...
double minRange = this.autoRangeMinimumSize.doubleValue();
if (range<minRange) {
upper = (upper+lower+minRange)/2;
lower = (upper+lower-minRange)/2;
}

if (this.autoRangeIncludesZero()) {
if (upper!=0.0) upper = Math.max(0.0, upper+upperMargin*range);
if (lower!=0.0) lower = Math.min(0.0, lower-lowerMargin*range);
}
else {
if (upper!=0.0) upper = upper+upperMargin*range;
if (lower!=0.0) lower = lower-lowerMargin*range;
}

if(logScale) {
lower = (lower <=0)?vvp.getMinimumVerticalDataValue().doubleValue():lower;
upper = (upper <=0)?vvp.getMaximumVerticalDataValue().doubleValue():upper;
}

this.minimumAxisValue=lower;
this.maximumAxisValue=upper;
}
}

}

/**
* Draws the plot on a Java 2D graphics device (such as the screen or a printer).
* @param g2 The graphics device;
* @param drawArea The area within which the chart should be drawn.
* @param plotArea The area within which the plot should be drawn (a subset of the drawArea).
*/
public void draw(Graphics2D g2, Rectangle2D drawArea, Rectangle2D plotArea) {

// draw the axis label
if (this.label!=null) {
g2.setFont(labelFont);
g2.setPaint(labelPaint);

Rectangle2D labelBounds = labelFont.getStringBounds(label, g2.getFontRenderContext());
if (labelDrawnVertical) {
double xx = drawArea.getX()+labelInsets.left+labelBounds.getHeight();
double yy = plotArea.getY()+plotArea.getHeight()/2+(labelBounds.getWidth()/2);
drawVerticalString(label, g2, (float)xx, (float)yy);
}
else {
double xx = drawArea.getX()+labelInsets.left;
double yy = drawArea.getY()+drawArea.getHeight()/2-labelBounds.getHeight()/2;
g2.drawString(label, (float)xx, (float)yy);
}
}

// draw the tick labels and marks and gridlines
this.refreshTicks(g2, drawArea, plotArea);
double xx = plotArea.getX();
g2.setFont(tickLabelFont);

Iterator iterator = ticks.iterator();
while (iterator.hasNext()) {
Tick tick = (Tick)iterator.next();
float yy = (float)this.translateValueToJava2D(tick.getNumericalValue(), plotArea);
if (tickLabelsVisible) {
g2.setPaint(this.tickLabelPaint);
g2.drawString(tick.getText(), tick.getX(), tick.getY());
}
if (tickMarksVisible) {
g2.setStroke(this.getTickMarkStroke());
Line2D mark = new Line2D.Double(plotArea.getX()-2, yy,
plotArea.getX()+2, yy);
g2.draw(mark);
}
if (gridLinesVisible) {
g2.setStroke(gridStroke);
g2.setPaint(gridPaint);
Line2D gridline = new Line2D.Double(xx, yy,
plotArea.getMaxX(), yy);
g2.draw(gridline);

}
}

}

/**
* Returns the width required to draw the axis in the specified draw area.
* @param g2 The graphics device;
* @param plot A reference to the plot;
* @param drawArea The area within which the plot should be drawn.
*/
public double reserveWidth(Graphics2D g2, Plot plot, Rectangle2D drawArea) {

// calculate the width of the axis label...
double labelWidth = 0.0;
if (label!=null) {
Rectangle2D labelBounds = labelFont.getStringBounds(label, g2.getFontRenderContext());
labelWidth = labelInsets.left+labelInsets.right;
if (this.labelDrawnVertical) {
labelWidth = labelWidth + labelBounds.getHeight(); // assume width == height before rotation
}
else {
labelWidth = labelWidth + labelBounds.getWidth();
}
}

// calculate the width required for the tick labels (if visible);
double tickLabelWidth = tickLabelInsets.left+tickLabelInsets.right;
if (tickLabelsVisible) {
this.refreshTicks(g2, drawArea, drawArea);
tickLabelWidth = tickLabelWidth+getMaxTickLabelWidth(g2, drawArea);
}
return labelWidth+tickLabelWidth;

}

/**
* Returns area in which the axis will be displayed.
* @param g2 The graphics device;
* @param plot A reference to the plot;
* @param drawArea The area in which the plot and axes should be drawn;
* @param reservedHeight The height reserved for the horizontal axis;
*/
public Rectangle2D reserveAxisArea(Graphics2D g2, Plot plot, Rectangle2D drawArea,
double reservedHeight) {

// calculate the width of the axis label...
double labelWidth = 0.0;
if (label!=null) {
Rectangle2D labelBounds = labelFont.getStringBounds(label, g2.getFontRenderContext());
labelWidth = labelInsets.left+labelInsets.right;
if (this.labelDrawnVertical) {
labelWidth = labelWidth + labelBounds.getHeight(); // assume width == height before rotation
}
else {
labelWidth = labelWidth + labelBounds.getWidth();
}
}

// calculate the width of the tick labels
double tickLabelWidth = tickLabelInsets.left+tickLabelInsets.right;
if (tickLabelsVisible) {
Rectangle2D approximatePlotArea = new Rectangle2D.Double(drawArea.getX(), drawArea.getY(),
drawArea.getWidth(),
drawArea.getHeight()-reservedHeight);
this.refreshTicks(g2, drawArea, approximatePlotArea);
tickLabelWidth = tickLabelWidth+getMaxTickLabelWidth(g2, approximatePlotArea);
}

return new Rectangle2D.Double(drawArea.getX(), drawArea.getY(), labelWidth+tickLabelWidth,
drawArea.getHeight()-reservedHeight);

}

/**
* Selects an appropriate tick value for the axis. The strategy is to display as many ticks as
* possible (selected from an array of 'standard' tick units) without the labels overlapping.
* @param g2 The graphics device;
* @param drawArea The area in which the plot and axes should be drawn;
* @param plotArea The area in which the plot should be drawn;
*/
private void selectAutoTickUnit(Graphics2D g2, Rectangle2D drawArea, Rectangle2D plotArea) {

// calculate the tick label height...
FontRenderContext frc = g2.getFontRenderContext();
double tickLabelHeight = tickLabelFont.getLineMetrics("123", frc).getHeight()
+this.tickLabelInsets.top+this.tickLabelInsets.bottom;

double threshold = tickLabelHeight;
double zero;
NumberTickUnit candidate1;
NumberTickUnit candidate2;
NumberTickUnit previnc;
NumberTickUnit inc;
double diff;
double olddiff;
double maxdata;
double tranmaxdata;

if(this.logScale) {
/* max data value */
maxdata = ((NumberTickUnit)this.standardTickUnits.getNearestTickUnit(((VerticalValuePlot)plot).getMaximumVerticalDataValue())).getValue().doubleValue();
tranmaxdata = this.translateValueToJava2D(maxdata, plotArea);
/* initial increment */
inc = (NumberTickUnit)this.standardTickUnits.getNearestTickUnit(new Double(1));
diff = this.translateValueToJava2D(maxdata - inc.getValue().doubleValue(), plotArea) - tranmaxdata;
olddiff = diff;

if(diff > threshold) {
while(diff > threshold) {
inc = (NumberTickUnit)this.standardTickUnits.getNextLowerTickUnit(inc);
diff = this.translateValueToJava2D(maxdata - inc.getValue().doubleValue(), plotArea) - tranmaxdata;
if(olddiff == diff) {
break;
}
else {
olddiff = diff;
}
}
this.tickUnit = (NumberTickUnit)this.standardTickUnits.getNextHigherTickUnit(inc);
}
else {
while(diff <= threshold) {
inc = (NumberTickUnit)this.standardTickUnits.getNextHigherTickUnit(inc);
diff = this.translateValueToJava2D(maxdata - inc.getValue().doubleValue(), plotArea) - tranmaxdata;
if(olddiff == diff) {
break;
}
else {
olddiff = diff;
}
}
this.tickUnit = (NumberTickUnit)this.standardTickUnits.getNextLowerTickUnit(inc);
}
}
else {
// now find the smallest tick unit that will accommodate the labels...
zero = this.translateValueToJava2D(0.0, plotArea);

// start with the current tick unit...
candidate1 = (NumberTickUnit)this.standardTickUnits.getNearestTickUnit(this.tickUnit);

double y = this.translateValueToJava2D(candidate1.getValue().doubleValue(), plotArea);
double unitHeight = Math.abs(y-zero);

// then extrapolate...
double bestguess = (threshold/unitHeight) * candidate1.value.doubleValue();
NumberTickUnit guess = new NumberTickUnit(new Double(bestguess), null);
candidate2 = (NumberTickUnit)this.standardTickUnits.getNearestTickUnit(guess);

this.tickUnit = candidate2;
}
}

/**
* Calculates the positions of the tick labels for the axis, storing the results in the
* tick label list (ready for drawing).
* @param g2 The graphics device.
* @param drawArea The area in which the plot and the axes should be drawn.
* @param plotArea The area in which the plot should be drawn.
*/
public void refreshTicks(Graphics2D g2, Rectangle2D drawArea, Rectangle2D plotArea) {

this.ticks.clear();

g2.setFont(tickLabelFont);

if (this.autoTickUnitSelection) {
selectAutoTickUnit(g2, drawArea, plotArea);
}

double size = this.tickUnit.getValue().doubleValue();
int count = this.calculateVisibleTickCount();
double lowestTickValue = this.calculateLowestVisibleTickValue();
//tickLabelFormatter = new DecimalFormat(tickLabelFormatter.toPattern());
for (int i=0; i<count; i++) {
Number currentTickValue = new Double(lowestTickValue+(i*size));
double yy = this.translateValueToJava2D(currentTickValue.doubleValue(), plotArea);
String tickLabel = this.valueToString(currentTickValue.doubleValue());
Rectangle2D tickLabelBounds = tickLabelFont.getStringBounds(tickLabel,
g2.getFontRenderContext());
float x = (float)(plotArea.getX()
-tickLabelBounds.getWidth()
-tickLabelInsets.left-tickLabelInsets.right);
float y = (float)(yy+(tickLabelBounds.getHeight()/2));
Tick tick = new Tick(currentTickValue, tickLabel, x, y);
ticks.add(tick);
}

}

/**
* Returns true if the specified plot is compatible with the axis, and false otherwise.
* <P>
* This class (VerticalNumberAxis) requires that the plot implements the VerticalValuePlot
* interface.
* @param plot The plot.
* @return True if the specified plot is compatible with the axis, and false otherwise.
*/
protected boolean isCompatiblePlot(Plot plot) {

if (plot instanceof VerticalValuePlot) return true;
else return false;

}

}

David Gilbert

Re: When is log-log to be added

Post by David Gilbert » Mon Mar 11, 2002 4:55 pm

Update: I contacted the developer regarding the logarithmic axis and he said he will send me the code later this week.

I notice that Mansheung Fung has also posted some code in this thread. I haven't tried it out yet, but I will get to it at some point.

Regards,

DG.

Ned Field

Re: When is log-log to be added

Post by Ned Field » Wed Apr 03, 2002 10:34 pm

I too would really like to have log-log plotting. Any idea when it might be available?

David Gilbert

Re: When is log-log to be added

Post by David Gilbert » Wed Apr 03, 2002 10:43 pm

There is a class VerticalLogarithmicAxis class in version 0.8.0, contributed by Mike Duffy. At some point I'll write a corresponding HorizontalLogarithmicAxis...unless someone else does it first.

Regards,

DG.

Even

Re: When is log-log to be added

Post by Even » Thu Apr 04, 2002 2:56 pm

It would be an nice option if we could change to logarithmic scale automaticly in the GUI.

Regards,
Even

Locked