Hybrid bar/scatter chart
davejbur opened this issue · 6 comments
Sorry, I opened this issue on the jcommon repo, as that was where my initial thoughts seemed to direct me (changing org.jfree.util.ShapeUtilities.java). However, on consideration, it might be better off in the actual jfreechart repo.
(Is it possible simply to reassign an issue from one repo to another? or is it better simply to copy it over here & close the other one?)
As indicated here, JCommon
is no longer separate. Does the problem occur using the current release, v1.5.3?
all the classes from
JCommon
that are used by JFreeChart have integrated within the JFreeChart jar file within a different package than before (you will need to change your imports);
OK, thanks for pointing out regarding jcommon - don't know how I got to thinking it was still needed, must have been from copying an old example originally.
Anyway, the example you mention does indeed allow different shapes for different points in the same series. However, unless I'm missing something, it doesn't allow for a shape whose height is determined by its yvalue and the current scale of the chart - which is effectively what a bar chart is.
I'm currently looking at creating a custom version of XYLineAndShapeRendererBar to see if it can do what I want. If it works I'll share it!
Can you use an XYBarRenderer
or perhaps combine it with another?
Thanks for that!
Yes, it's a bit messy but it works. I would share/push all the code, but it's not a pretty sight, and doesn't cover all cases...
I had to create my own version of XYLineAndShapeRenderer, adding in code pinched from XYBarRenderer to draw bars for those series where I wanted them. There needs to be a flag somewhere; at present I've simply checked whether the itemShape is a Rectangle2D or not - if it is, do the new process.
I've also had to create custom versions of GradientXYBarPainter, StandardXYBarPainter and XYBarPainter - and even then I've not covered all the code that would need to be changed for a comprehensive new hybrid scatter/bar component. Still, it's got me going for now!
If anyone's interested, the main code addition is as follows (after Shape shape = getItemShape(series, item);
in drawSecondaryPass
):
if ("java.awt.geom.Rectangle2D$Double".equals(shape.getClass().getName()))
{
//fix the height
Rectangle2D bounds2D = shape.getBounds2D();
double x = bounds2D.getX();
double y = bounds2D.getY();
double w = bounds2D.getWidth();
double h = bounds2D.getHeight();
// IntervalXYDataset intervalDataset = (IntervalXYDataset) dataset;
double value0=0.0;
double value1=y1;
//FIXME - ignoring for now the concept of bars that don't start from 0
/* double value0;
double value1;
if (this.useYInterval) {
value0 = intervalDataset.getStartYValue(series, item);
value1 = intervalDataset.getEndYValue(series, item);
} else {
value0 = this.base;
value1 = intervalDataset.getYValue(series, item);
}*/
if (Double.isNaN(value0) || Double.isNaN(value1)) {
return;
}
if (value0 <= value1) {
if (!rangeAxis.getRange().intersects(value0, value1)) {
return;
}
} else {
if (!rangeAxis.getRange().intersects(value1, value0)) {
return;
}
}
double translatedValue0 = rangeAxis.valueToJava2D(value0, dataArea,
plot.getRangeAxisEdge());
double translatedValue1 = rangeAxis.valueToJava2D(value1, dataArea,
plot.getRangeAxisEdge());
double bottom = Math.min(translatedValue0, translatedValue1);
double top = Math.max(translatedValue0, translatedValue1);
/* double startX = intervalDataset.getStartXValue(series, item);
if (Double.isNaN(startX)) {
return;
}
double endX = x1;
double endX = intervalDataset.getEndXValue(series, item);
if (Double.isNaN(endX)) {
return;
}
if (startX <= endX) {
if (!domainAxis.getRange().intersects(startX, endX)) {
return;
}
} else {
if (!domainAxis.getRange().intersects(endX, startX)) {
return;
}
}*/
if (!domainAxis.getRange().contains(x1)) {
return;
}
//FIXME - ignoring for now any bar alignment adjustments
// is there an alignment adjustment to be made?
/* if (this.barAlignmentFactor >= 0.0 && this.barAlignmentFactor <= 1.0) {
double x = intervalDataset.getXValue(series, item);
double interval = endX - startX;
startX = x - interval * this.barAlignmentFactor;
endX = startX + interval;
}*/
RectangleEdge location = plot.getDomainAxisEdge();
double translatedX = domainAxis.valueToJava2D(x1, dataArea,
location);
double translatedWidth = w;
double left = translatedX;
/* double translatedStartX = domainAxis.valueToJava2D(startX, dataArea,
location);
double translatedEndX = domainAxis.valueToJava2D(endX, dataArea,
location);
double translatedWidth = Math.max(1, Math.abs(translatedEndX
- translatedStartX));
double left = Math.min(translatedStartX, translatedEndX);*/
//FIXME - ignoring for now any bar width adjustments
/* if (getMargin() > 0.0) {
double cut = translatedWidth * getMargin();
translatedWidth = translatedWidth - cut;
left = left + cut / 2;
}*/
Rectangle2D bar = null;
// PlotOrientation orientation = plot.getOrientation();
if (orientation.isHorizontal()) {
// clip left and right bounds to data area
bottom = Math.max(bottom, dataArea.getMinX());
top = Math.min(top, dataArea.getMaxX());
bar = new Rectangle2D.Double(
bottom, left, top - bottom, translatedWidth);
} else if (orientation.isVertical()) {
// clip top and bottom bounds to data area
bottom = Math.max(bottom, dataArea.getMinY());
top = Math.min(top, dataArea.getMaxY());
bar = new Rectangle2D.Double(left, bottom, translatedWidth,
top - bottom);
}
Rectangle2D bounds2D1 = bar.getBounds2D();
boolean positive = (value1 > 0.0);
boolean inverted = rangeAxis.isInverted();
RectangleEdge barBase;
if (orientation.isHorizontal()) {
if (positive && inverted || !positive && !inverted) {
barBase = RectangleEdge.RIGHT;
} else {
barBase = RectangleEdge.LEFT;
}
} else {
if (positive && !inverted || !positive && inverted) {
barBase = RectangleEdge.BOTTOM;
} else {
barBase = RectangleEdge.TOP;
}
}
if (state.getElementHinting()) {
beginElementGroup(g2, dataset.getSeriesKey(series), item);
}
//FIXME - ignoring for now any shadows
/* if (getShadowsVisible()) {
this.barPainter.paintBarShadow(g2, this, series, item, bar, barBase,
!this.useYInterval);
}*/
this.barPainter.paintBar(g2, this, series, item, bar, barBase);
if (state.getElementHinting()) {
endElementGroup(g2);
}
//FIXME - ignoring for now item labels
/* if (isItemLabelVisible(series, item)) {
XYItemLabelGenerator generator = getItemLabelGenerator(series,
item);
drawItemLabel(g2, dataset, series, item, plot, generator, bar,
value1 < 0.0);
}*/
// update the crosshair point
double x1ch = w / 2.0;
// double x1ch = (startX + endX) / 2.0;
double y1ch = dataset.getYValue(series, item);
double transX1ch = domainAxis.valueToJava2D(x1ch, dataArea, location);
double transY1ch = rangeAxis.valueToJava2D(y1ch, dataArea,
plot.getRangeAxisEdge());
int datasetIndex = plot.indexOf(dataset);
updateCrosshairValues(crosshairState, x1ch, y1ch, datasetIndex,
transX1ch, transY1ch, plot.getOrientation());
// EntityCollection entities = state.getEntityCollection();
if (entities != null) {
addEntity(entities, bar, dataset, series, item, 0.0, 0.0);
}
}
else
{
(code then continues with the if (orientation == PlotOrientation.HORIZONTAL) {
block)
I've left in the original code from XYBarRenderer in comments so it's easier to compare.
a plot can have more than one renderer
Thank you! I hadn't realised that...!
The final code required no fiddling round customising the internal JFreeChart. My solution, simplified to remove irrelevant customising of series colours/markers etc, is:
XYLineAndShapeRenderer xyScatterRenderer = new XYLineAndShapeRenderer(false, true);
XYBarRenderer xyBarRenderer = new XYBarRenderer();
xyBarRenderer.setDefaultToolTipGenerator(new StandardXYToolTipGenerator());
xyScatterRenderer.setDefaultToolTipGenerator(new StandardXYToolTipGenerator());
NumberAxis xAxis = new NumberAxis("Dist (m)");
NumberAxis yAxis = new NumberAxis("Force (kN)");
XYPlot xyPlot = new XYPlot(xyScatterDataset,xAxis,yAxis,xyScatterRenderer);
xyPlot.setDataset(1,xyBarDataset);
xyPlot.setRenderer(1,xyBarRenderer);
xyPlot.setDatasetRenderingOrder(DatasetRenderingOrder.FORWARD);
scatterChart = new JFreeChart(theTitle, JFreeChart.DEFAULT_TITLE_FONT, xyPlot, true);
Many thanks again, that's a great help.