I want to show in a CombinedDomainXYPlot a marker. This is not working. I can add the marker for each subplot. But I want to add it for the CombinedDomainXYPlot. Anybody who can tell me something about this issue. I think with the crosshair the behaviour is the same.
This is working example which creates a CombinedDomainXYPlot and tries to add a marker. The marker adding is in createCombinedChart()
import java.awt.Font;
import javax.swing.JPanel;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.ChartUtilities;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.annotations.XYTextAnnotation;
import org.jfree.chart.axis.AxisLocation;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.plot.CombinedDomainXYPlot;
import org.jfree.chart.plot.IntervalMarker;
import org.jfree.chart.plot.Marker;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.StandardXYItemRenderer;
import org.jfree.chart.renderer.xy.XYItemRenderer;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
import org.jfree.ui.ApplicationFrame;
import org.jfree.ui.RefineryUtilities;
/**
* A demonstration application showing how to create a combined chart.
*/
public class MarkerDemo2 extends ApplicationFrame {
/**
* Constructs a new demonstration application.
*
* #param title the frame title.
*/
public MarkerDemo2(String title) {
super(title);
JFreeChart chart = createCombinedChart();
ChartPanel panel = (ChartPanel) createDemoPanel();
panel.setPreferredSize(new java.awt.Dimension(500, 270));
setContentPane(panel);
}
/**
* Creates a combined chart.
*
* #return The combined chart.
*/
private static JFreeChart createCombinedChart() {
// create subplot 1...
XYDataset data1 = createDataset1();
XYItemRenderer renderer1 = new StandardXYItemRenderer();
NumberAxis rangeAxis1 = new NumberAxis("Range 1");
XYPlot subplot1 = new XYPlot(data1, null, rangeAxis1, renderer1);
subplot1.setRangeAxisLocation(AxisLocation.BOTTOM_OR_LEFT);
// add secondary axis
subplot1.setDataset(1, createDataset2());
NumberAxis axis2 = new NumberAxis("Range Axis 2");
axis2.setAutoRangeIncludesZero(false);
subplot1.setRangeAxis(1, axis2);
subplot1.setRangeAxisLocation(1, AxisLocation.BOTTOM_OR_RIGHT);
subplot1.setRenderer(1, new StandardXYItemRenderer());
subplot1.mapDatasetToRangeAxis(1, 1);
XYTextAnnotation annotation = new XYTextAnnotation("Hello!", 50.0, 10000.0);
annotation.setFont(new Font("SansSerif", Font.PLAIN, 9));
annotation.setRotationAngle(Math.PI / 4.0);
subplot1.addAnnotation(annotation);
// create subplot 2...
XYDataset data2 = createDataset2();
XYItemRenderer renderer2 = new StandardXYItemRenderer();
NumberAxis rangeAxis2 = new NumberAxis("Range 2");
rangeAxis2.setAutoRangeIncludesZero(false);
XYPlot subplot2 = new XYPlot(data2, null, rangeAxis2, renderer2);
subplot2.setRangeAxisLocation(AxisLocation.TOP_OR_LEFT);
// parent plot...
CombinedDomainXYPlot plot = new CombinedDomainXYPlot(new NumberAxis("Domain"));
plot.setGap(10.0);
// add the subplots...
plot.add(subplot1, 1);
plot.add(subplot2, 1);
plot.setOrientation(PlotOrientation.VERTICAL);
// Add marker
Marker marker = new IntervalMarker(30, 40);
plot.addDomainMarker(marker);
// Working
// subplot1.addDomainMarker(marker);
// return a new chart containing the overlaid plot...
JFreeChart chart = new JFreeChart("CombinedDomainXYPlot Demo",
JFreeChart.DEFAULT_TITLE_FONT, plot, true);
ChartUtilities.applyCurrentTheme(chart);
return chart;
}
/**
* Creates a sample dataset.
*
* #return Series 1.
*/
private static XYDataset createDataset1() {
// create dataset 1...
XYSeries series1 = new XYSeries("Series 1a");
series1.add(10.0, 12353.3);
series1.add(20.0, 13734.4);
series1.add(30.0, 14525.3);
series1.add(40.0, 13984.3);
series1.add(50.0, 12999.4);
series1.add(60.0, 14274.3);
series1.add(70.0, 15943.5);
series1.add(80.0, 14845.3);
series1.add(90.0, 14645.4);
series1.add(100.0, 16234.6);
series1.add(110.0, 17232.3);
series1.add(120.0, 14232.2);
series1.add(130.0, 13102.2);
series1.add(140.0, 14230.2);
series1.add(150.0, 11235.2);
XYSeries series1b = new XYSeries("Series 1b");
series1b.add(10.0, 15000.3);
series1b.add(20.0, 11000.4);
series1b.add(30.0, 17000.3);
series1b.add(40.0, 15000.3);
series1b.add(50.0, 14000.4);
series1b.add(60.0, 12000.3);
series1b.add(70.0, 11000.5);
series1b.add(80.0, 12000.3);
series1b.add(90.0, 13000.4);
series1b.add(100.0, 12000.6);
series1b.add(110.0, 13000.3);
series1b.add(120.0, 17000.2);
series1b.add(130.0, 18000.2);
series1b.add(140.0, 16000.2);
series1b.add(150.0, 17000.2);
XYSeriesCollection collection = new XYSeriesCollection();
collection.addSeries(series1);
collection.addSeries(series1b);
return collection;
}
/**
* Creates a sample dataset.
*
* #return A sample dataset.
*/
private static XYDataset createDataset2() {
// create dataset 2...
XYSeries series2 = new XYSeries("Series 2");
series2.add(10.0, 6853.2);
series2.add(20.0, 9642.3);
series2.add(30.0, 8253.5);
series2.add(40.0, 5352.3);
series2.add(50.0, 3532.0);
series2.add(60.0, 2635.3);
series2.add(70.0, 3998.2);
series2.add(80.0, 1943.2);
series2.add(90.0, 6943.9);
series2.add(100.0, 7843.2);
series2.add(105.0, 6495.3);
series2.add(110.0, 7943.6);
series2.add(115.0, 8500.7);
series2.add(120.0, 9595.9);
return new XYSeriesCollection(series2);
}
/**
* Creates a panel for the demo (used by SuperDemo.java).
*
* #return A panel.
*/
public static JPanel createDemoPanel() {
JFreeChart chart = createCombinedChart();
ChartPanel panel = new ChartPanel(chart);
panel.setMouseWheelEnabled(true);
return new ChartPanel(chart);
}
/**
* Starting point for the demonstration application.
*
* #param args ignored.
*/
public static void main(String[] args) {
MarkerDemo2 demo = new MarkerDemo2(
"JFreeChart: CombinedDomainXYPlotDemo4.java");
demo.pack();
RefineryUtilities.centerFrameOnScreen(demo);
demo.setVisible(true);
}
}
drawAnnotations is implemented in XYPlot, unfortunately CombinedDomainXYPlot subclasses XYPlot but doesn't call drawAnnotations in its overridden implementation of draw or forward the annotation on to the subplots.
You could possible provide your on implemntation of addAnnotation and removeAnnotation by subclassing CombinedDomainXYPlot.
Thanks to Graham I developed an adjusted version of CombinedDomainXYPlot, which works for me. The result looks like this.
I merged Code from XYPlot and XYLineAndShapeRenderer to achieve this.
Here is the source code.
import java.awt.AlphaComposite;
import java.awt.Composite;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.jfree.chart.annotations.XYAnnotation;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.CombinedDomainXYPlot;
import org.jfree.chart.plot.IntervalMarker;
import org.jfree.chart.plot.Marker;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.PlotRenderingInfo;
import org.jfree.chart.plot.PlotState;
import org.jfree.chart.plot.ValueMarker;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYItemRenderer;
import org.jfree.data.Range;
import org.jfree.text.TextUtilities;
import org.jfree.ui.GradientPaintTransformer;
import org.jfree.ui.Layer;
import org.jfree.ui.LengthAdjustmentType;
import org.jfree.ui.RectangleAnchor;
import org.jfree.ui.RectangleInsets;
public class MyCombinedDomainXYPlot extends CombinedDomainXYPlot {
public MyCombinedDomainXYPlot() {
super();
// TODO Auto-generated constructor stub
}
public MyCombinedDomainXYPlot(ValueAxis domainAxis) {
super(domainAxis);
// TODO Auto-generated constructor stub
}
#Override
public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
PlotState parentState, PlotRenderingInfo info) {
super.draw(g2, area, anchor, parentState, info);
drawDomainMarkers(g2, area, 0, Layer.FOREGROUND);
}
/**
* Draws the domain markers (if any) for an axis and layer. This method is
* typically called from within the draw() method.
*
* #param g2 the graphics device.
* #param dataArea the data area.
* #param index the renderer index.
* #param layer the layer (foreground or background).
*/
#Override
protected void drawDomainMarkers(Graphics2D g2, Rectangle2D dataArea,
int index, Layer layer) {
// check that the renderer has a corresponding dataset (it doesn't
// matter if the dataset is null)
if (index >= getDatasetCount()) {
return;
}
Collection markers = getDomainMarkers(index, layer);
ValueAxis axis = getDomainAxisForDataset(index);
if (markers != null && axis != null) {
Iterator iterator = markers.iterator();
while (iterator.hasNext()) {
Marker marker = (Marker) iterator.next();
drawDomainMarker(g2, marker, dataArea);
}
}
}
/**
* Draws a vertical line on the chart to represent a 'range marker'.
*
* #param g2
* the graphics device.
* #param plot
* the plot.
* #param domainAxis
* the domain axis.
* #param marker
* the marker line.
* #param dataArea
* the axis data area.
*/
public void drawDomainMarker(Graphics2D g2, Marker marker, Rectangle2D dataArea) {
ValueAxis domainAxis = getDomainAxis();
if (marker instanceof ValueMarker) {
ValueMarker vm = (ValueMarker) marker;
double value = vm.getValue();
Range range = domainAxis.getRange();
if (!range.contains(value)) {
return;
}
double v = domainAxis.valueToJava2D(value, dataArea,
getDomainAxisEdge());
PlotOrientation orientation = getOrientation();
Line2D line = null;
if (orientation == PlotOrientation.HORIZONTAL) {
line = new Line2D.Double(dataArea.getMinX(), v,
dataArea.getMaxX(), v);
} else if (orientation == PlotOrientation.VERTICAL) {
line = new Line2D.Double(v, dataArea.getMinY(), v,
dataArea.getMaxY());
}
final Composite originalComposite = g2.getComposite();
g2.setComposite(AlphaComposite.getInstance(
AlphaComposite.SRC_OVER, marker.getAlpha()));
g2.setPaint(marker.getPaint());
g2.setStroke(marker.getStroke());
g2.draw(line);
String label = marker.getLabel();
RectangleAnchor anchor = marker.getLabelAnchor();
if (label != null) {
Font labelFont = marker.getLabelFont();
g2.setFont(labelFont);
g2.setPaint(marker.getLabelPaint());
Point2D coordinates = calculateDomainMarkerTextAnchorPoint(
g2, orientation, dataArea, line.getBounds2D(),
marker.getLabelOffset(),
LengthAdjustmentType.EXPAND, anchor);
TextUtilities.drawAlignedString(label, g2,
(float) coordinates.getX(),
(float) coordinates.getY(),
marker.getLabelTextAnchor());
}
g2.setComposite(originalComposite);
} else if (marker instanceof IntervalMarker) {
IntervalMarker im = (IntervalMarker) marker;
double start = im.getStartValue();
double end = im.getEndValue();
Range range = domainAxis.getRange();
if (!(range.intersects(start, end))) {
return;
}
double start2d = domainAxis.valueToJava2D(start, dataArea,
getDomainAxisEdge());
double end2d = domainAxis.valueToJava2D(end, dataArea,
getDomainAxisEdge());
double low = Math.min(start2d, end2d);
double high = Math.max(start2d, end2d);
PlotOrientation orientation = getOrientation();
Rectangle2D rect = null;
if (orientation == PlotOrientation.HORIZONTAL) {
// clip top and bottom bounds to data area
low = Math.max(low, dataArea.getMinY());
high = Math.min(high, dataArea.getMaxY());
rect = new Rectangle2D.Double(dataArea.getMinX(), low,
dataArea.getWidth(), high - low);
} else if (orientation == PlotOrientation.VERTICAL) {
// clip left and right bounds to data area
low = Math.max(low, dataArea.getMinX());
high = Math.min(high, dataArea.getMaxX());
rect = new Rectangle2D.Double(low, dataArea.getMinY(), high
- low, dataArea.getHeight());
}
final Composite originalComposite = g2.getComposite();
g2.setComposite(AlphaComposite.getInstance(
AlphaComposite.SRC_OVER, marker.getAlpha()));
Paint p = marker.getPaint();
if (p instanceof GradientPaint) {
GradientPaint gp = (GradientPaint) p;
GradientPaintTransformer t = im
.getGradientPaintTransformer();
if (t != null) {
gp = t.transform(gp, rect);
}
g2.setPaint(gp);
} else {
g2.setPaint(p);
}
g2.fill(rect);
// now draw the outlines, if visible...
if (im.getOutlinePaint() != null
&& im.getOutlineStroke() != null) {
if (orientation == PlotOrientation.VERTICAL) {
Line2D line = new Line2D.Double();
double y0 = dataArea.getMinY();
double y1 = dataArea.getMaxY();
g2.setPaint(im.getOutlinePaint());
g2.setStroke(im.getOutlineStroke());
if (range.contains(start)) {
line.setLine(start2d, y0, start2d, y1);
g2.draw(line);
}
if (range.contains(end)) {
line.setLine(end2d, y0, end2d, y1);
g2.draw(line);
}
} else { // PlotOrientation.HORIZONTAL
Line2D line = new Line2D.Double();
double x0 = dataArea.getMinX();
double x1 = dataArea.getMaxX();
g2.setPaint(im.getOutlinePaint());
g2.setStroke(im.getOutlineStroke());
if (range.contains(start)) {
line.setLine(x0, start2d, x1, start2d);
g2.draw(line);
}
if (range.contains(end)) {
line.setLine(x0, end2d, x1, end2d);
g2.draw(line);
}
}
}
String label = marker.getLabel();
RectangleAnchor anchor = marker.getLabelAnchor();
if (label != null) {
Font labelFont = marker.getLabelFont();
g2.setFont(labelFont);
g2.setPaint(marker.getLabelPaint());
Point2D coordinates = calculateDomainMarkerTextAnchorPoint(
g2, orientation, dataArea, rect,
marker.getLabelOffset(),
marker.getLabelOffsetType(), anchor);
TextUtilities.drawAlignedString(label, g2,
(float) coordinates.getX(),
(float) coordinates.getY(),
marker.getLabelTextAnchor());
}
g2.setComposite(originalComposite);
}
}
/**
* Calculates the (x, y) coordinates for drawing a marker label.
*
* #param g2
* the graphics device.
* #param orientation
* the plot orientation.
* #param dataArea
* the data area.
* #param markerArea
* the rectangle surrounding the marker area.
* #param markerOffset
* the marker label offset.
* #param labelOffsetType
* the label offset type.
* #param anchor
* the label anchor.
*
* #return The coordinates for drawing the marker label.
*/
protected Point2D calculateDomainMarkerTextAnchorPoint(Graphics2D g2,
PlotOrientation orientation, Rectangle2D dataArea,
Rectangle2D markerArea, RectangleInsets markerOffset,
LengthAdjustmentType labelOffsetType, RectangleAnchor anchor) {
Rectangle2D anchorRect = null;
if (orientation == PlotOrientation.HORIZONTAL) {
anchorRect = markerOffset.createAdjustedRectangle(markerArea,
LengthAdjustmentType.CONTRACT, labelOffsetType);
} else if (orientation == PlotOrientation.VERTICAL) {
anchorRect = markerOffset.createAdjustedRectangle(markerArea,
labelOffsetType, LengthAdjustmentType.CONTRACT);
}
return RectangleAnchor.coordinates(anchorRect, anchor);
}
}
Related
I have the following chart:
The chart is dynamic and has the capability of make zoom in and zoom out.
I want to put an icon that is created as:
BufferedImage triangleIcon = null;
try {
triangleIcon = ImageIO.read(getClass().getClassLoader().getResource("Resources/Imagenes/triangulo.png"));
} catch (IOException e) {
}
And can be placed in any point as:
xyannotation = new XYImageAnnotation(XValue, YValue, triangleIcon);
this.xyplot.addAnnotation(xyannotation);
I want to put this icon at any Y value but always next to the left side of the chart, without taking into account the zoom. Something like:
Is it possible?
I looks like you want to annotate "one side of the…chart, ignoring the value of the X axis". Ordinarily, such annotations specify both coordinates in data space. One approach is a custom annotation that extends the abstract parent, AbstractXYAnnotation. Override draw() to transform the vertical data coordinate to the corresponding screen coordinate, while pinning the horizontal screen coordinate to the dataArea of the plot. Pan and zoom to see the effect.
AffineTransform at = new AffineTransform();
…
#Override
public void draw(…) {
RectangleEdge rangeEdge = plot.getRangeAxisEdge();
double y = rangeAxis.valueToJava2D(yValue, dataArea, rangeEdge);
at.setToIdentity();
at.translate(dataArea.getX() + 1, y);
…
}
Example code:
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics2D;
import java.awt.Polygon;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import javax.swing.JFrame;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.annotations.AbstractXYAnnotation;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.PlotRenderingInfo;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.ui.RectangleEdge;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
/**
* #see https://stackoverflow.com/q/72311226/230513
* #see https://stackoverflow.com/q/71780485/230513
*/
public class XYShapeTest {
private static final int N = 18_000;
private static final int SIZE = 16;
private final Shape pointer = createPolygon(SIZE);
private static class AxisAnnotation extends AbstractXYAnnotation {
private final AffineTransform at = new AffineTransform();
private final Stroke stroke = new BasicStroke(4f);
private final Shape shape;
private final double yValue;
public AxisAnnotation(Shape shape, double y) {
this.shape = shape;
this.yValue = y;
}
#Override
public void draw(Graphics2D g2, XYPlot plot, Rectangle2D dataArea,
ValueAxis domainAxis, ValueAxis rangeAxis,
int rendererIndex, PlotRenderingInfo info) {
RectangleEdge rangeEdge = plot.getRangeAxisEdge();
double y = rangeAxis.valueToJava2D(yValue, dataArea, rangeEdge);
at.setToIdentity();
at.translate(dataArea.getX() + 1, y);
g2.setStroke(stroke);
g2.setPaint(Color.BLACK);
g2.draw(at.createTransformedShape(shape));
}
}
private Shape createPolygon(int size) {
Polygon p = new Polygon();
p.addPoint(0, 0);
p.addPoint(0, 1);
p.addPoint(2, 0);
p.addPoint(0, -1);
AffineTransform at = new AffineTransform();
at.scale(size, size);
return at.createTransformedShape(p);
}
private XYDataset createDataset() {
XYSeries series = new XYSeries("Series");
for (int i = 0; i <= N; i += 1_000) {
series.add(i / 1_000, i);
}
return new XYSeriesCollection(series);
}
private JFreeChart createChart(final XYDataset dataset) {
JFreeChart chart = ChartFactory.createXYLineChart("Test", "X", "Y", dataset);
XYPlot plot = (XYPlot) chart.getPlot();
plot.setDomainPannable(true);
plot.setRangePannable(true);
plot.addAnnotation(new AxisAnnotation(pointer, 9_000));
return chart;
}
private void display() {
JFrame f = new JFrame("XYShapeTest");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(new ChartPanel(createChart(createDataset())) {
#Override
public Dimension getPreferredSize() {
return new Dimension(600, 300);
}
});
f.pack();
f.setLocationRelativeTo(null);
f.setVisible(true);
}
public static void main(String[] args) {
EventQueue.invokeLater(new XYShapeTest()::display);
}
}
I have a 3D dataset XYZDataset that I want to plot as a 2D plot, by keeping (x,y) coordinates and by representing the z axis using a spectrum of colors.
Based on this example, here is my ploting class along with the spectrum color class.
package com.ingilab.algo.comparator.tools.plot;
import java.awt.Color;
import java.awt.Paint;
import java.awt.Shape;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.AxisLocation;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.PaintScale;
import org.jfree.chart.renderer.xy.XYBlockRenderer;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.chart.title.PaintScaleLegend;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
import org.jfree.data.xy.XYZDataset;
import org.jfree.ui.ApplicationFrame;
import org.jfree.ui.RectangleEdge;
import org.jfree.ui.RectangleInsets;
import org.jfree.util.ShapeUtilities;
public class Plot2D extends ApplicationFrame {
private static final int N = 100;
/**
* A demonstration application showing an XY series containing a null value.
*
* #param title the frame title.
*/
final XYSeries series;
public Plot2D(final String title, String X, String Y, XYSeries series) {
super(title);
this.series = series;
final XYSeriesCollection data = new XYSeriesCollection(series);
final JFreeChart chart = ChartFactory.createScatterPlot(
title,
X,
Y,
data,
PlotOrientation.VERTICAL,
true,
true,
false
);
final ChartPanel chartPanel = new ChartPanel(chart);
chartPanel.setPreferredSize(new java.awt.Dimension(500, 270));
setContentPane(chartPanel);
}
public Plot2D (final String title, JFreeChart chart) {
super(title);
series = null;
final ChartPanel chartPanel = new ChartPanel(chart);
chartPanel.setPreferredSize(new java.awt.Dimension(500, 270));
setContentPane(chartPanel);
}
/**
* Creates a sample chart.
*
* #param dataset the dataset.
* #param max
*
* #return A sample chart.
*/
public static JFreeChart createChart(XYZDataset dataset,
String title, String x, String y, String z, double max) {
NumberAxis xAxis = new NumberAxis(x);
xAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
xAxis.setLowerMargin(0.0);
xAxis.setUpperMargin(0.0);
NumberAxis yAxis = new NumberAxis(y);
yAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
yAxis.setLowerMargin(0.0);
yAxis.setUpperMargin(0.0);
XYBlockRenderer renderer = new XYBlockRenderer();
SpectrumPaintScale scale = new SpectrumPaintScale(0, max);
//PaintScale scale = new GrayPaintScale(-2.0, 1.0);
renderer.setPaintScale(scale);
//Z axis
NumberAxis scaleAxis = new NumberAxis(z);
scaleAxis.setAxisLinePaint(Color.white);
scaleAxis.setTickMarkPaint(Color.white);
PaintScaleLegend legend = new PaintScaleLegend(scale, scaleAxis);
legend.setSubdivisionCount(128);
legend.setAxisLocation(AxisLocation.TOP_OR_RIGHT);
legend.setPadding(new RectangleInsets(10, 10, 10, 10));
legend.setStripWidth(20);
legend.setPosition(RectangleEdge.RIGHT);
legend.setBackgroundPaint(Color.WHITE);
XYPlot plot = new XYPlot(dataset, xAxis, yAxis, renderer);
plot.setBackgroundPaint(Color.lightGray);
plot.setDomainGridlinesVisible(false);
plot.setRangeGridlinePaint(Color.white);
plot.setRenderer(new XYLineAndShapeRenderer(false, true) {
#Override
public Shape getItemShape(int row, int col) {
return ShapeUtilities.createDiagonalCross(5, 2);
}
});
JFreeChart chart = new JFreeChart(title, plot);
chart.addSubtitle(legend);
chart.removeLegend();
chart.setBackgroundPaint(Color.white);
return chart;
}
////////////////////////////////////
// //
// PaintScaleColor //
// //
////////////////////////////////////
private static class SpectrumPaintScale implements PaintScale {
private static final float H1 = 0f;
private static final float H2 = 1f;
private final double lowerBound;
private final double upperBound;
public SpectrumPaintScale(double lowerBound, double upperBound) {
this.lowerBound = lowerBound;
this.upperBound = upperBound;
}
#Override
public double getLowerBound() {
return lowerBound;
}
#Override
public double getUpperBound() {
return upperBound;
}
#Override
public Paint getPaint(double value) {
float scaledValue = (float) (value / (getUpperBound() - getLowerBound()));
float scaledH = H1 + scaledValue * (H2 - H1);
return Color.getHSBColor(scaledH, 1f, 1f);
}
}
public static void main(String[] args)
{
final DefaultXYZDataset timePerSizePerChrno = new
DefaultXYZDataset();
ydb [0][1] = 1;
ydb [1][1] = 78.0;
ydb [2][1] = 1341.0;
ydb [0][2] = 2;
ydb [1][2] = 100.0;
ydb [2][2] = 475.0;
ydb [0][1] = 3;
ydb [1][1] = 9215.0;
ydb [2][1] = 684.0;
ydb [0][1] = 4;
ydb [1][1] = 90.0;
ydb [2][1] = 251.0;
ydb [0][1] = 5;
ydb [1][1] = 75.0;
ydb [2][1] = 7022.0;
double maxZ = 7022;
timePerSizePerChrno.addSeries("Series", ydb);
//////////////////////////////////////////
// PLOTING RESUlTS //
//////////////////////////////////////////
final Plot2D plot3 = new Plot2D("Loading Performance Color Map", Plot2D.createChart (timePerSizePerChrno,
"Loading Performance Color Map", "Order Call", "Time in Ms", "Size in Ko", maxZ));
plot3.pack();
RefineryUtilities.centerFrameOnScreen(plot3);
plot3.setVisible(true);
}
}
The problem I am having is that the spectrum of colors is applied on the XYZDataset series and not on the z values (I have one unique serie in my dataset).
For instance on the above image. You can see that all points are in red and I want them to be mapped to the spectrum of color on the right based on their values. I also want to remove the red at the end of the spectrum since it can be confusing (the spectrum starts and finishes with the red color).
Any guess on how for a given series, plot the different point (x,y) using a spectrum of color knowing that z values are between [0, maxZ].
Your updated example creates an XYBlockRenderer, as shown here, and applies a custom PaintScale to the renderer; the scale is also used to create a matching PaintScaleLegend. After using the XYBlockRenderer to create an XYPlot, the original XYBlockRenderer is discarded and replaced with an XYLineAndShapeRenderer, which overrides getItemShape(). The new XYLineAndShapeRenderer knows nothing about the PaintScale.
Instead, override getItemFillPaint() in your XYLineAndShapeRenderer, as shown here. Instead of the List<Color> shown, use the getPaint() method of your custom PaintScale to interpolate the desired Shape color for each data point based on its corresponding z value.
XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer(false, true) {
#Override
public Paint getItemFillPaint(int row, int col) {
return scale.getPaint(dataset.getZValue(row, col));
}
…
};
In addition,
To get a different spectrum, specify the desired boundary hues in the PaintScale.
private static final float H1 = 0f;
private static final float H2 = (float) (Math.PI / 8);
Use DatasetUtils.findZBounds() to determine the dataset range.
Range r = DatasetUtils.findZBounds(dataset);
Construct and manipulate Swing GUI objects only on the event dispatch thread.
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Paint;
import java.awt.Shape;
import javax.swing.JFrame;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.AxisLocation;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.PaintScale;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.chart.title.PaintScaleLegend;
import org.jfree.data.Range;
import org.jfree.data.xy.DefaultXYZDataset;
import org.jfree.data.xy.XYZDataset;
import org.jfree.chart.ui.RectangleEdge;
import org.jfree.chart.ui.RectangleInsets;
import org.jfree.chart.util.ShapeUtils;
import org.jfree.data.general.DatasetUtils;
/**
* #see https://stackoverflow.com/a/54180207/230513
* #see https://stackoverflow.com/a/37235165/230513
*/
public class Plot2D {
public static JFreeChart createChart(XYZDataset dataset,
String title, String x, String y, String z, Range r) {
NumberAxis xAxis = new NumberAxis(x);
xAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
NumberAxis yAxis = new NumberAxis(y);
yAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
SpectrumPaintScale scale = new SpectrumPaintScale(r);
NumberAxis scaleAxis = new NumberAxis(z);
scaleAxis.setAxisLinePaint(Color.white);
scaleAxis.setTickMarkPaint(Color.white);
PaintScaleLegend legend = new PaintScaleLegend(scale, scaleAxis);
legend.setSubdivisionCount(128);
legend.setAxisLocation(AxisLocation.TOP_OR_RIGHT);
legend.setPadding(new RectangleInsets(10, 10, 10, 10));
legend.setStripWidth(20);
legend.setPosition(RectangleEdge.RIGHT);
legend.setBackgroundPaint(Color.WHITE);
XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer(false, true) {
#Override
public Paint getItemFillPaint(int row, int col) {
return scale.getPaint(dataset.getZValue(row, col));
}
#Override
public Shape getItemShape(int row, int col) {
return ShapeUtils.createDiagonalCross(5, 2);
}
};
renderer.setUseFillPaint(true);
renderer.setSeriesShapesFilled(0, true);
renderer.setSeriesShapesVisible(0, true);
XYPlot plot = new XYPlot(dataset, xAxis, yAxis, renderer);
plot.setBackgroundPaint(Color.lightGray);
plot.setDomainGridlinesVisible(false);
plot.setRangeGridlinePaint(Color.white);
plot.setRenderer((renderer));
JFreeChart chart = new JFreeChart(title, plot);
chart.addSubtitle(legend);
chart.removeLegend();
chart.setBackgroundPaint(Color.white);
return chart;
}
private static class SpectrumPaintScale implements PaintScale {
private static final float H1 = 0f;
private static final float H2 = (float) (Math.PI / 8);
private final Range range;
public SpectrumPaintScale(Range r) {
this.range = r;
}
#Override
public double getLowerBound() {
return range.getLowerBound();
}
#Override
public double getUpperBound() {
return range.getUpperBound();
}
#Override
public Paint getPaint(double value) {
float scaledValue = (float) (value / (getUpperBound() - getLowerBound()));
float scaledH = H1 + scaledValue * (H2 - H1);
return Color.getHSBColor(scaledH, 1f, 1f);
}
}
public static void main(String[] args) {
double xyz[][] = {
{ 1, 2, 3, 4, 5 }, // x
{ 1000, 3000, 9215, 4000, 1000 }, // y
{ 1341, 500, 3125, 1000, 7022 } // z
};
final DefaultXYZDataset dataset = new DefaultXYZDataset();
dataset.addSeries("Series", xyz);
Range r = DatasetUtils.findZBounds(dataset);
EventQueue.invokeLater(() -> {
JFrame f = new JFrame("Color Map");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JFreeChart chart = Plot2D.createChart(dataset, "Color Map",
"Order Call", "Time in Ms", "Size in Ko", r);
f.add(new ChartPanel(chart) {
#Override
public Dimension getPreferredSize() {
return new Dimension(600, 300);
}
});
f.pack();
f.setLocationRelativeTo(null);
f.setVisible(true);
});
}
}
You may not be able to tell that much but for some reason a few of the characters are being offset somehow, due to a flaw in my algorithm...If someone could figure out what is causing it, I would really appreciate it and any critique is welcome as I'm still very new at java.
Edit: If you look at the image above it's the E that is offset in WE on the left and right side
Edit: I think it may be in my calculation of the size of text vs size of circle
Edit: Ok so when I enter 600 for width and height everything seems to fall in place, but as it gets smaller from say 250 for example the characters start becoming more offset and overlapping
Main class:
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
/**
* Created by John on 7/11/2014.
*/
public class Prog14_05 extends Application {
#Override
public void start(Stage primaryStage) {
// Create Pane
circularText phrase = new circularText("WE ARE ANONYMOUS, " +
"WE ARE LEGION, WE DO NOT FORGIVE, WE DO NOT FORGET ",
480, 480);
// Place clock and label in border pane
GridPane pane = new GridPane();
pane.setPadding(new Insets(phrase.getTextSize() * 2));
pane.setAlignment(Pos.CENTER);
pane.setStyle("-fx-background-color: black");
pane.getChildren().add(phrase);
// Create a scene and place it in the stage
Scene scene = new Scene(pane);
primaryStage.setTitle("Exercise14_05");
primaryStage.setScene(scene);
primaryStage.show();
}
}
circularText Class:
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
/**
* Created by John on 7/11/2014.
*/
public class circularText extends Pane {
double textSize = 30;
String string = "";
String fontName = "";
Font font;
// Pane's width and height
private double w = 250, h = 250;
/** Create Constructor */
public circularText (String phrase, double w, double h) {
this.w = w;
this.h = h;
this.string = phrase;
textSize = (this.w / this.string.length()) * 2;
Font font = new Font("Times Roman", textSize);
paintText(this.string, this.font);
}
/** Set new font */
public void setFont(String name) {
Font font = new Font(name, textSize);
this.font = font;
this.fontName = name;
paintText(this.string, this.font);
}
/** Return textSize */
public double getTextSize() {
return this.textSize;
}
/** Set textSize */
public void setTextSize(double textSize) {
this.textSize = textSize;
Font font = new Font(fontName, textSize);
this.font = font;
paintText(this.string, this.font);
}
/** Return pane's width */
public double getW() {
return w;
}
/** Set pane's width */
public void setW(double w) {
this.w = w;
textSize = (this.w / this.string.length()) * 2;
paintText(this.string, this.font);
}
/** Return pane's height */
public double getH() {
return h;
}
/** Set pane's height */
public void setH(double h) {
this.h = h;
textSize = (this.w / this.string.length()) * 2;
paintText(this.string, this.font);
}
/** Paint the Letters */
protected void paintText(String phrase, Font font) {
// Initialize parameters
double radius = Math.min(w, h) * 0.8 * 0.5;
double centerX = w / 2;
double centerY = h / 2;
double size = radius / 4 - this.getTextSize();
// Draw circle
Circle circle = new Circle(centerX - size - textSize, centerY - size,
radius);
circle.setFill(null);
circle.setStroke(null);
getChildren().clear();
getChildren().add(circle);
// Place text in a circular pattern
int i = 0;
double degree = 360 / phrase.length();
for (double degrees = 0; i < phrase.length(); i++, degrees += degree) {
double pointX = circle.getCenterX() + circle.getRadius() *
Math.cos(Math.toRadians(degrees));
double pointY = circle.getCenterY() + circle.getRadius() *
Math.sin(Math.toRadians(degrees));
Text letter = new Text(pointX, pointY, phrase.charAt(i) + "");
letter.setFont(font);
letter.setFill(Color.LIME);
letter.setRotate(degrees + 90);
getChildren().add(letter);
}
}
}
My trig isn't very good so I can't help you there. I'm thinking the "W" may be offset, not the "E". I know in other versions of Swing the "W" has caused painting problems before, but I don't remember the details. So I might suggest trying different characters to see if you still have the same problem at those two locations.
Here is another example of circular painting that I found on the web a long time ago. I tried your text and the "WE" is overlapped. I changed the "W" to an "R" and it seems to work ok, so maybe this validates my above statement?
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import java.awt.Font;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GradientPaint;
import java.awt.RenderingHints;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.awt.geom.AffineTransform;
//import com.sun.awt.AWTUtilities;
public class SplashPortalPanel6 extends JPanel
{
private static final long serialVersionUID = 1L;
// private static final char[] MESSAGE = " SplashPortal.net".toCharArray();
private static final char[] MESSAGE = " WE ARE ANONYMOUS, WE ARE LEGION, WE DO NOT FORGIVE, WE DO NOT FORGET ".toCharArray();
// private static final char[] MESSAGE = " RE ARE ANONYMOUS, RE ARE LEGION, RE DO NOT FORGIVE, RE DO NOT FORGET ".toCharArray();
private static final double R90 = Math.toRadians(90);
private static final double R_90 = Math.toRadians(-90);
private AffineTransform cumalativeRotation = new AffineTransform();
private double rotation = Math.toRadians(360.0 / MESSAGE.length);
private Font font = new Font("Impact",Font.ITALIC,40);
private final Timer timer = new Timer(1000/76, new ActionListener() {
public void actionPerformed(ActionEvent e) {
repaint();//just repaint
}
});
public SplashPortalPanel6() {
setPreferredSize(new java.awt.Dimension(600, 600));
setOpaque(false);
}
//This method is called when the panel is connected to a native
//screen resource. It's an indication we can now start painting.
public void addNotify() {
super.addNotify();
timer.start();
}
public void removeNotify() {
super.removeNotify();
timer.stop();
}
private static final GradientPaint gradient = new GradientPaint(0F, 0F, Color.BLUE, 5F, 10F, Color.CYAN, true);
private static final int x = 0, y = 0, w = 100, h = 100;
public void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2.setFont(font);
g2.translate( getWidth()/2, getHeight()/2 );
cumalativeRotation.rotate(rotation/50);
g2.transform( cumalativeRotation );
for(int i = 0; i < MESSAGE.length; i++) {
// fill the rectangle
g2.translate(250, 0);
g2.rotate(R90);
g2.setColor(Color.BLACK);
// g2.fillRect(x,y,w,h);
// draw the border
g2.setColor(Color.WHITE);
// g2.drawRect(x,y,w,h);
// draw the character
g2.setPaint(gradient);
g2.drawChars(MESSAGE,i, 1, x+30, y+50);
g2.rotate(R_90);
g2.translate(-250, 0);
g2.rotate(rotation);
}
}
public static void createAndShowSplashScreen() {
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setUndecorated(true);
frame.setContentPane(new SplashPortalPanel6());
frame.pack();
frame.setLocationRelativeTo(null);
// AWTUtilities.setWindowOpaque(frame, false);
//frame.setAlwaysOnTop(true);
frame.setVisible(true);
}
public static void main(String[] args) {
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
createAndShowSplashScreen();
}
});
}
}
Note, if you uncomment the "fillRect" and "drawRect" statements you will see the original implementation of the code. Of course you will need to use the shorter first message string to see the effect.
Add
letter.setTextAlignment(TextAlignment.CENTER);
letter.setWrappingWidth(100);
Not sure what is going on with JavaFX Text rendering.
The math appears correct. For clarity when coding some suggest adding explicit typing to ensure your not mixing doubles with floats with ints. So instead of
double centerY = h / 2;
do
double centerY = h / 2.0d;
(Also take out extra "this." clutter, many methods are not being used like "setH", and make class name upper case CircularText)
Alright here is what I got so far, let me know what you all think and what can be improved upon...
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
/**
* Created by John on 7/11/2014.
*/
public class CircularText extends Pane {
private double circleWidth;
private double circleHeight;
private double textSize;
private double textStartDegree;
private double textRotate;
private double gapSpacing;
private double offSetX;
private double offSetY;
private Font font;
private Paint textFill;
private String fontName;
final String text;
/** Default Constructor */
public CircularText(String text) {
this.circleWidth = 250;
this.circleHeight = 250;
this.text = text;
textSize = (this.circleWidth / this.text.length()) * 2;
this.font = new Font("Times Roman", textSize);
this.textFill = Color.BLACK;
this.textStartDegree = 240;
this.textRotate = 90;
this.gapSpacing = 0.975;
this.offSetX = 4;
this.offSetY = 3;
paintText(this.text, this.font);
}
/** Create Constructor */
public CircularText (String text, double w, double h) {
this.circleWidth = w;
this.circleHeight = h;
this.text = text;
textSize = (this.circleWidth / (this.text.length()) * 2);
this.font = new Font("Times Roman", textSize);
this.textFill = Color.BLACK;
this.textStartDegree = 240;
this.textRotate = 90;
this.gapSpacing = 0.975;
this.offSetX = 4;
this.offSetY = 3;
paintText(this.text, this.font);
}
/** Get font color */
public Paint getTextFill() {
return textFill;
}
/** Set font color */
public void setTextFill(Paint textFill) {
this.textFill = textFill;
this.font = new Font(fontName, textSize);
paintText(this.text, this.font);
}
/** Get starting position for text */
public double getTextStartDegree() {
return textStartDegree;
}
/** Set starting position for text */
public void setTextStartDegree(double textStartDegree) {
this.textStartDegree = textStartDegree;
this.font = new Font(fontName, textSize);
paintText(this.text, this.font);
}
/** Get letter rotation */
public double getTextRotate() {
return textRotate;
}
/** Set letter rotation */
public void setTextRotate(double textRotate) {
this.textRotate = textRotate;
this.font = new Font(fontName, textSize);
paintText(this.text, this.font);
}
/** Get spacing between ending and beginning of phrase */
public double getGapSpacing() {
return gapSpacing;
}
/** Set spacing between ending and beginning of phrase */
public void setGapSpacing(double gapSpacing) {
this.gapSpacing = gapSpacing;
this.font = new Font(fontName, textSize);
paintText(this.text, this.font);
}
/** Get current font */
public Font getFont() {
return this.font;
}
/** Set new font */
public void setFont(String name) {
this.font = new Font(name, textSize);
this.fontName = name;
paintText(this.text, this.font);
}
/** Return textSize */
public double getTextSize() {
return this.textSize;
}
/** Set textSize */
public void setTextSize(double textSize, double offSetX, double offSetY) {
this.textSize = textSize;
this.offSetX = offSetX;
this.offSetY = offSetY;
this.font = new Font(fontName, textSize);
paintText(this.text, this.font);
}
/** Return circle's width */
public double getCircleWidth() {
return circleWidth;
}
/** Set circle's width */
public void setCircleWidth(double w) {
this.circleWidth = w;
textSize = (this.circleWidth / this.text.length()) * 2;
paintText(this.text, this.font);
}
/** Return circle's height */
public double getCircleHeight() {
return circleHeight;
}
/** Set circle's height */
public void setCircleHeight(double h) {
this.circleHeight = h;
textSize = (this.circleWidth / this.text.length()) * 2;
paintText(this.text, this.font);
}
/** Paint the Letters */
protected void paintText(String text, Font font) {
getChildren().clear();
// Initialize parameters
double radius = Math.min(circleWidth, circleHeight) * 0.8 * 0.5;
double centerX = circleWidth / 2;
double centerY = circleHeight / 2;
// Place text in a circular pattern
int i = 0;
double degree = 360.0 / (text.length() / this.gapSpacing);
for (double degrees = this.textStartDegree;
i < text.length(); i++, degrees += degree) {
double pointX = centerX + radius *
Math.cos(Math.toRadians(degrees)) - (this.textSize) *
this.offSetX;
double pointY = centerY + radius *
Math.sin(Math.toRadians(degrees)) - (this.textSize) *
this.offSetY;
Text letter = new Text(pointX, pointY,
String.valueOf(text.charAt(i)));
letter.setFont(font);
letter.setFill(this.textFill);
letter.setRotate(degrees + this.textRotate);
letter.setTextAlignment(TextAlignment.CENTER);
getChildren().add(letter);
}
}
}
After testing this with Courier New font, it appears to render flawlessly. I also tested this with other fonts and everything still rendered correctly. It appears the flaw in my code was correlated with the Circle object I had created for troubleshooting and for some reason decided to use in my algorithm. After removing this Circle object and fixing small flaws in my code and adding flexibility, everything works perfectly :)
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.layout.GridPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
/**
* Created by John on 7/11/2014.
*/
public class Prog14_05 extends Application {
#Override
public void start(Stage primaryStage) {
// Create Pane
CircularText phrase = new CircularText("TESTING MY CIRCULAR" +
"TEXT OBJECT CLASS",
500, 500);
phrase.setFont("Courier New");
phrase.setTextFill(Color.LIME);
phrase.setTextSize(20);
// Place clock and label in border pane
GridPane pane = new GridPane();
pane.setPadding(new Insets(phrase.getTextSize()));
pane.setAlignment(Pos.CENTER);
pane.setStyle("-fx-background-color: black");
pane.getChildren().add(phrase);
// Create a scene and place it in the stage
Scene scene = new Scene(pane);
primaryStage.setTitle("Exercise14_05");
primaryStage.setScene(scene);
primaryStage.show();
}
}
P.S Test my code and let me know if you find any bugs, Thanks for everyone's help
I am trying to draw a curved Line arrow on a stacked bar graph.I have been able to draw the curved line and arrow.But i am not able to connect the arrow to the end of the curved line.I am using affine transformation to draw the curved line.The below link describes the curved line and arrow that i have been able to draw http://i58.tinypic.com/2m422hy.png.Can anyone guide me as to how to connect the arrow to the end of the curved line.
Here is the code
package Stack;
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
/**
*
* #author OSPL-B4
/
/
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Polygon;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.QuadCurve2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import org.jfree.chart.annotations.CategoryAnnotation;
import org.jfree.chart.axis.CategoryAnchor;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.event.AnnotationChangeListener;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.Plot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.data.category.CategoryDataset;
import org.jfree.io.SerialUtilities;
import org.jfree.ui.RectangleEdge;
import org.jfree.util.ObjectUtilities;
import org.jfree.util.PaintUtilities;
//import java.awt.Font;
/**
* A line annotation that can be placed on a
* {#link org.jfree.chart.plot.CategoryPlot}.
*/
public class CategoryLineAnnotation_demo1 implements CategoryAnnotation,
Cloneable, Serializable {
/** The category for the start of the line. */
private Comparable category1;
/** The value for the start of the line. */
private double value1;
/** The category for the end of the line. */
private Comparable category2;
/** The value for the end of the line. */
private double value2;
private final int ARR_SIZE = 4;
/** The line color. */
private transient Paint paint = Color.black;
/** The line stroke. */
private transient Stroke stroke = new BasicStroke(1.0f);
/**
* Creates a new annotation that draws a line between (category1, value1)
* and (category2, value2).
*
* #param category1 the category (<code>null</code> not permitted).
* #param value1 the value.
* #param category2 the category (<code>null</code> not permitted).
* #param value2 the value.
*/
public CategoryLineAnnotation_demo1(Comparable category1, double value1,
Comparable category2, double value2,
Paint paint, Stroke stroke) {
if (category1 == null) {
throw new IllegalArgumentException("Null 'category1' argument.");
}
if (category2 == null) {
throw new IllegalArgumentException("Null 'category2' argument.");
}
if (paint == null) {
throw new IllegalArgumentException("Null 'paint' argument.");
}
if (stroke == null) {
throw new IllegalArgumentException("Null 'stroke' argument.");
}
this.category1 = category1;
System.out.println("First Category value is "+category1);
this.value1 = value1;
this.category2 = category2;
System.out.println("Second Category value is "+category2);
this.value2 = value2;
this.paint = paint;
this.stroke = stroke;
}
/**
* Returns the category for the start of the line.
*
* #return The category for the start of the line (never <code>null</code>).
*/
public Comparable getCategory1() {
return this.category1;
}
/**
* Sets the category for the start of the line.
*
* #param category the category (<code>null</code> not permitted).
*/
public void setCategory1(Comparable category) {
if (category == null) {
throw new IllegalArgumentException("Null 'category' argument.");
}
this.category1 = category;
}
/**
* Returns the y-value for the start of the line.
*
* #return The y-value for the start of the line.
*/
public double getValue1() {
return this.value1;
}
/**
* Sets the y-value for the start of the line.
*
* #param value the value.
*/
public void setValue1(double value) {
this.value1 = value;
}
/**
* Returns the category for the end of the line.
*
* #return The category for the end of the line (never <code>null</code>).
*/
public Comparable getCategory2() {
return this.category2;
}
/**
* Sets the category for the end of the line.
*
* #param category the category (<code>null</code> not permitted).
*/
public void setCategory2(Comparable category) {
if (category == null) {
throw new IllegalArgumentException("Null 'category' argument.");
}
this.category2 = category;
}
/**
* Returns the y-value for the end of the line.
*
* #return The y-value for the end of the line.
*/
public double getValue2() {
return this.value2;
}
/**
* Sets the y-value for the end of the line.
*
* #param value the value.
*/
public void setValue2(double value) {
this.value2 = value;
}
/**
* Returns the paint used to draw the connecting line.
*
* #return The paint (never <code>null</code>).
*/
public Paint getPaint() {
return this.paint;
}
/**
* Sets the paint used to draw the connecting line.
*
* #param paint the paint (<code>null</code> not permitted).
*/
public void setPaint(Paint paint) {
if (paint == null) {
throw new IllegalArgumentException("Null 'paint' argument.");
}
this.paint = paint;
}
/**
* Returns the stroke used to draw the connecting line.
*
* #return The stroke (never <code>null</code>).
*/
public Stroke getStroke() {
// System.out.println("In Stacked bar Stroke is "+getStroke());
return this.stroke;
}
/**
* Sets the stroke used to draw the connecting line.
*
* #param stroke the stroke (<code>null</code> not permitted).
*/
public void setStroke(Stroke stroke) {
if (stroke == null) {
throw new IllegalArgumentException("Null 'stroke' argument.");
}
this.stroke = stroke;
}
/**
* Draws the annotation.
*
* #param g2 the graphics device.
* #param plot the plot.
* #param dataArea the data area.
* #param domainAxis the domain axis.
* #param rangeAxis the range axis.
*/
public void draw(Graphics2D g2, CategoryPlot plot, Rectangle2D dataArea,
CategoryAxis domainAxis, ValueAxis rangeAxis) {
CategoryDataset dataset = plot.getDataset();
int catIndex1 = dataset.getColumnIndex(this.category1);
int catIndex2 = dataset.getColumnIndex(this.category2);
int catCount = dataset.getColumnCount();
double lineX1 = 0.0f;
double lineY1 = 0.0f;
double lineX2 = 0.0f;
double lineY2 = 0.0f;
PlotOrientation orientation = plot.getOrientation();
RectangleEdge domainEdge = Plot.resolveDomainAxisLocation(
plot.getDomainAxisLocation(), orientation);
RectangleEdge rangeEdge = Plot.resolveRangeAxisLocation(
plot.getRangeAxisLocation(), orientation);
if (orientation == PlotOrientation.HORIZONTAL) {
lineY1 = domainAxis.getCategoryJava2DCoordinate(
CategoryAnchor.MIDDLE, catIndex1, catCount, dataArea,
domainEdge);
lineX1 = rangeAxis.valueToJava2D(this.value1, dataArea, rangeEdge);
lineY2 = domainAxis.getCategoryJava2DCoordinate(
CategoryAnchor.MIDDLE, catIndex2, catCount, dataArea,
domainEdge);
lineX2 = rangeAxis.valueToJava2D(this.value2, dataArea, rangeEdge);
}
else if (orientation == PlotOrientation.VERTICAL) {
lineX1 = domainAxis.getCategoryJava2DCoordinate(
CategoryAnchor.MIDDLE, catIndex1, catCount, dataArea,
domainEdge);
lineY1 = rangeAxis.valueToJava2D(this.value1, dataArea, rangeEdge);
lineX2 = domainAxis.getCategoryJava2DCoordinate(
CategoryAnchor.MIDDLE, catIndex2, catCount, dataArea,
domainEdge);
lineY2 = rangeAxis.valueToJava2D(this.value2, dataArea, rangeEdge);
}
g2.setPaint(this.paint);
g2.setStroke(this.stroke);
drawArrow(g2,(int) lineX1, (int) lineY1, (int) lineX2, (int) lineY2);
}
void drawArrow(Graphics g1, int x1, int y1, int x2, int y2) {
Graphics2D g = (Graphics2D) g1.create();
double dx = x2 - x1, dy = y2 - y1;
System.out.println("Value of DX "+dx);
System.out.println("Value of DY "+dy);
double angle = Math.atan2(dy, dx);
System.out.println("Getting angle "+angle);
int len = (int) Math.sqrt(dx*dx + dy*dy);
AffineTransform at = AffineTransform.getTranslateInstance(x1, y1);
at.concatenate(AffineTransform.getRotateInstance(angle));
g.transform(at);
System.out.println("Affine transform X co-ordinate value is "+at.getScaleX());
System.out.println("Affine transform Y co-ordinate value is "+at.getScaleY());
float center1=(x1+x2)/2-40;
float center2= (y1+y2)/2-40;
QuadCurve2D q=new QuadCurve2D.Float(0,0,center1,center2,x2,y2);
g.draw(q);
g.setColor(Color.RED);
System.out.println("Length of arrow is "+len);
System.out.println("Get Start point 2D "+q.getP1());
System.out.println("Get End point 2D "+q.getP2());
g.fillPolygon(new int[] {len, len-ARR_SIZE, len-ARR_SIZE-10, len-60},
new int[] {0, -ARR_SIZE, ARR_SIZE-20, 5}, 4);
}
public void paintComponent(Graphics g) {
for (int x = 15; x < 200; x += 16)
drawArrow(g, x, x, x, 150);
drawArrow(g, 30, 300, 300, 190);
}
/**
* Tests this object for equality with another.
*
* #param obj the object (<code>null</code> permitted).
*
* #return <code>true</code> or <code>false</code>.
*/
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof CategoryLineAnnotation_demo1)) {
return false;
}
CategoryLineAnnotation_demo1 that = (CategoryLineAnnotation_demo1) obj;
if (!this.category1.equals(that.getCategory1())) {
return false;
}
if (this.value1 != that.getValue1()) {
return false;
}
if (!this.category2.equals(that.getCategory2())) {
return false;
}
if (this.value2 != that.getValue2()) {
return false;
}
if (!PaintUtilities.equal(this.paint, that.paint)) {
return false;
}
if (!ObjectUtilities.equal(this.stroke, that.stroke)) {
return false;
}
return true;
}
/**
* Returns a hash code for this instance.
*
* #return A hash code.
*/
public int hashCode() {
// TODO: this needs work
return this.category1.hashCode() + this.category2.hashCode();
}
/**
* Returns a clone of the annotation.
*
* #return A clone.
*
* #throws CloneNotSupportedException this class will not throw this
* exception, but subclasses (if any) might.
*/
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
/**
* Provides serialization support.
*
* #param stream the output stream.
*
* #throws IOException if there is an I/O error.
*/
private void writeObject(ObjectOutputStream stream) throws IOException {
stream.defaultWriteObject();
SerialUtilities.writePaint(this.paint, stream);
SerialUtilities.writeStroke(this.stroke, stream);
}
/**
* Provides serialization support.
*
* #param stream the input stream.
*
* #throws IOException if there is an I/O error.
* #throws ClassNotFoundException if there is a classpath problem.
*/
private void readObject(ObjectInputStream stream)
throws IOException, ClassNotFoundException {
stream.defaultReadObject();
this.paint = SerialUtilities.readPaint(stream);
this.stroke = SerialUtilities.readStroke(stream);
}
#Override
public void addChangeListener(AnnotationChangeListener al) {
}
#Override
public void removeChangeListener(AnnotationChangeListener al) {
}
}
You can create the arrow head based on the last line segment (which might already be transformed using an AffineTransform)
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Shape;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class ArrowPainter
{
public static void main(String[] args)
{
SwingUtilities.invokeLater(new Runnable()
{
#Override
public void run()
{
createAndShowGUI();
}
});
}
private static void createAndShowGUI()
{
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel panel = new ArrowPaintPanel();
f.getContentPane().add(panel);
f.setSize(500,500);
f.setLocationRelativeTo(null);
f.setVisible(true);
}
}
class ArrowPaintPanel extends JPanel implements MouseMotionListener
{
private Point2D startPoint = null;
private Point2D endPoint = null;
ArrowPaintPanel()
{
addMouseMotionListener(this);
}
#Override
protected void paintComponent(Graphics gr)
{
super.paintComponent(gr);
Graphics2D g = (Graphics2D)gr;
if (startPoint == null)
{
startPoint = new Point(getWidth()/2, getHeight()/2);
}
if (endPoint == null)
{
return;
}
Line2D line = new Line2D.Double(startPoint, endPoint);
Shape arrowHead = createArrowHead(line, 30, 20);
g.draw(line);
g.fill(arrowHead);
}
#Override
public void mouseDragged(MouseEvent e)
{
endPoint = e.getPoint();
repaint();
}
#Override
public void mouseMoved(MouseEvent e)
{
endPoint = e.getPoint();
repaint();
}
private static Shape createArrowHead(Line2D line, double length, double width)
{
Point2D p0 = line.getP1();
Point2D p1 = line.getP2();
double x0 = p0.getX();
double y0 = p0.getY();
double x1 = p1.getX();
double y1 = p1.getY();
double dx = x1 - x0;
double dy = y1 - y0;
double invLength = 1.0 / Math.sqrt(dx*dx+dy*dy);
double dirX = dx * invLength;
double dirY = dy * invLength;
double ax = x1 - length * dirX;
double ay = y1 - length * dirY;
double offsetX = width * -dirY * 0.5;
double offsetY = width * dirX * 0.5;
double c0x = ax + offsetX;
double c0y = ay + offsetY;
double c1x = ax - offsetX;
double c1y = ay - offsetY;
Path2D arrowHead = new Path2D.Double();
arrowHead.moveTo(x1, y1);
arrowHead.lineTo(c0x, c0y);
arrowHead.lineTo(c1x, c1y);
arrowHead.closePath();
return arrowHead;
}
}
EDIT: Update for the above EDIT and the comments: That's a lot of code, but still nothing that can be tested easily. What happens when you replace your line
drawArrow(g2,(int) lineX1, (int) lineY1, (int) lineX2, (int) lineY2);
with
g.fill(createArrowHead(new Line2D.Double(lineX1, lineY1, lineX2, lineY2), 30, 20));
?
I plot a curve with JFreechart. Then the user can draw ranges by dragging the mouse. These I plot using AbstractChartAnnotation to draw a filled Path2D. So far so nice - all aligns perfectly with the curve.
When an area was already annotated the new annotation gets deleted. I use XYPlot.removeAnnotation with the new annotation.
My problem is that sometimes not only the "new" annotation gets removed, but also a second annotation elsewhere in the plot. It doesn't seem random - I kinda found annotations to the "right" side more prone to this happening.
I'm very confused what could cause this. The object that draws/deletes the new annotation is reinstated every time and only holds the current annotation - so how could the other annotation be deleted?
Would be very grateful for any hints, thanks.
As suggested I prepare a sscce example. Unfortunately it's not too short.
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.*;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.event.MouseInputListener;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.annotations.AbstractXYAnnotation;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.Plot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.PlotRenderingInfo;
import org.jfree.chart.plot.XYPlot;
import org.jfree.data.time.Millisecond;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;
import org.jfree.data.time.TimeSeriesDataItem;
import org.jfree.ui.RectangleEdge;
/**
*
* #author c.ager
*/
public class IntegrationSSCE {
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
JFrame jFrame = new JFrame();
jFrame.setLayout(new BorderLayout());
jFrame.setSize(600, 400);
jFrame.setDefaultCloseOperation(jFrame.EXIT_ON_CLOSE);
TimeSeriesCollection timeSeriesCollection = new TimeSeriesCollection();
TimeSeries timeSeries = new TimeSeries("test");
for (long i = 0; i < 1000; i++) {
double val = Math.random() + 3 * Math.exp(-Math.pow(i - 300, 2) / 1000);
timeSeries.add(new Millisecond(new Date(i)), val);
}
timeSeriesCollection.addSeries(timeSeries);
JFreeChart chart = ChartFactory.createTimeSeriesChart(
null,
null, "data", timeSeriesCollection,
true, true, false);
ChartPanel chartPanel = new ChartPanel(chart);
chartPanel.removeMouseListener(chartPanel);
Set<MyAnnot> annotSet = new TreeSet<MyAnnot>();
AnnotListener list = new AnnotListener(chartPanel, annotSet, timeSeries);
chartPanel.addMouseListener(list);
chartPanel.addMouseMotionListener(list);
jFrame.add(chartPanel, BorderLayout.CENTER);
jFrame.setVisible(true);
// TODO code application logic here
}
private static class AnnotListener implements MouseInputListener {
Point2D start, end;
MyAnnot currAnnot;
final Set<MyAnnot> annotSet;
final ChartPanel myChart;
final TimeSeries timeSeries;
public AnnotListener(ChartPanel myChart, Set<MyAnnot> annotSet, TimeSeries timeSeries) {
this.myChart = myChart;
this.annotSet = annotSet;
this.timeSeries = timeSeries;
}
#Override
public void mousePressed(MouseEvent e) {
start = convertScreePoint2DataPoint(e.getPoint());
currAnnot = new MyAnnot(start, timeSeries, myChart.getChart().getXYPlot());
myChart.getChart().getXYPlot().addAnnotation(currAnnot);
}
#Override
public void mouseDragged(MouseEvent e) {
end = convertScreePoint2DataPoint(e.getPoint());
currAnnot.updateEnd(end);
}
#Override
public void mouseReleased(MouseEvent e) {
boolean test = annotSet.add(currAnnot);
if (!test) {
myChart.getChart().getXYPlot().removeAnnotation(currAnnot);
}
}
#Override
public void mouseEntered(MouseEvent e) {
}
#Override
public void mouseClicked(MouseEvent e) {
}
#Override
public void mouseExited(MouseEvent e) {
}
#Override
public void mouseMoved(MouseEvent e) {
}
protected Point2D convertScreePoint2DataPoint(Point in) {
Rectangle2D plotArea = myChart.getScreenDataArea();
XYPlot plot = (XYPlot) myChart.getChart().getPlot();
double x = plot.getDomainAxis().java2DToValue(in.getX(), plotArea, plot.getDomainAxisEdge());
double y = plot.getRangeAxis().java2DToValue(in.getY(), plotArea, plot.getRangeAxisEdge());
return new Point2D.Double(x, y);
}
}
private static class MyAnnot extends AbstractXYAnnotation implements Comparable<MyAnnot> {
Long max;
Line2D line;
final TimeSeries timeSeries;
final XYPlot plot;
final Stroke stroke = new BasicStroke(1.5f);
public MyAnnot(Point2D start, TimeSeries timeSeries, XYPlot plot) {
this.plot = plot;
this.timeSeries = timeSeries;
line = new Line2D.Double(start, start);
findMax();
}
public void updateEnd(Point2D end) {
line.setLine(line.getP1(), end);
findMax();
fireAnnotationChanged();
}
#Override
public void draw(Graphics2D gd, XYPlot xyplot, Rectangle2D rd, ValueAxis va, ValueAxis va1, int i, PlotRenderingInfo pri) {
PlotOrientation orientation = plot.getOrientation();
RectangleEdge domainEdge = Plot.resolveDomainAxisLocation(
plot.getDomainAxisLocation(), orientation);
RectangleEdge rangeEdge = Plot.resolveRangeAxisLocation(
plot.getRangeAxisLocation(), orientation);
double m02 = va.valueToJava2D(0, rd, domainEdge);
// y-axis translation
double m12 = va1.valueToJava2D(0, rd, rangeEdge);
// x-axis scale
double m00 = va.valueToJava2D(1, rd, domainEdge) - m02;
// y-axis scale
double m11 = va1.valueToJava2D(1, rd, rangeEdge) - m12;
Shape s = null;
if (orientation == PlotOrientation.HORIZONTAL) {
AffineTransform t1 = new AffineTransform(
0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f);
AffineTransform t2 = new AffineTransform(
m11, 0.0f, 0.0f, m00, m12, m02);
s = t1.createTransformedShape(line);
s = t2.createTransformedShape(s);
} else if (orientation == PlotOrientation.VERTICAL) {
AffineTransform t = new AffineTransform(m00, 0, 0, m11, m02, m12);
s = t.createTransformedShape(line);
}
gd.setStroke(stroke);
gd.setPaint(Color.BLUE);
gd.draw(s);
addEntity(pri, s.getBounds2D(), i, getToolTipText(), getURL());
}
#Override
public int compareTo(MyAnnot o) {
return max.compareTo(o.max);
}
private void findMax() {
max = (long) line.getP1().getX();
Point2D left, right;
if (line.getP1().getX() < line.getP2().getX()) {
left = line.getP1();
right = line.getP2();
} else {
left = line.getP2();
right = line.getP1();
}
Double maxVal = left.getY();
List<TimeSeriesDataItem> items = timeSeries.getItems();
for (Iterator<TimeSeriesDataItem> it = items.iterator(); it.hasNext();) {
TimeSeriesDataItem dataItem = it.next();
if (dataItem.getPeriod().getFirstMillisecond() < left.getX()) {
continue;
}
if (dataItem.getPeriod().getFirstMillisecond() > right.getX()) {
break;
}
double curVal = dataItem.getValue().doubleValue();
if (curVal > maxVal) {
maxVal = curVal;
max = dataItem.getPeriod().getFirstMillisecond();
}
}
}
}
}
Here is the problematic behaviour. Note that images 2 and 4 were taken while the mouse button was pressed.
select a few non-overlapping lines - no problem as it should be
I have just been looking at it in the debugger - could it be that ArrayList.remove(Object o) removes the WRONG element? Seems very unlikely to me...
You might look at the Layer to which the annotation is being added. There's an example here. Naturally, an sscce that exhibits the problem you describe would help clarify the source of the problem.
Addendum: One potential problem is that your implementation of Comparable is not consistent with equals(), as the latter relies (implicitly) on the super-class implementation. A consistent implementation is required for use with a sorted Set such as TreeSet. You'll need to override hashCode(), too. Class Value is an example.