jfreechart line chart curve does not turn on inflection points - jfreechart

I have a line chart and have it creating a smooth curve instead of a jagged graph. (I am doing this via renderers, XYSplineRenderer as an example)
The curve should have inflection points at the data points but rises above or below and does not look good and is misleading (implying that some data exists above the max and/or below the min)
Code that produces this:
import java.awt.Color;
import java.awt.Font;
import java.awt.Point;
import java.awt.geom.Ellipse2D;
import java.io.File;
import java.io.FileOutputStream;
import java.text.SimpleDateFormat;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartUtils;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.DateAxis;
import org.jfree.chart.axis.DateTickUnit;
import org.jfree.chart.axis.DateTickUnitType;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.TickUnits;
import org.jfree.chart.labels.XYItemLabelGenerator;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYSplineRenderer;
import org.jfree.data.time.Day;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;
import org.jfree.data.xy.XYDataset;
public class LineChart {
public String saveToFile(String path,String pathColor) {
try {
JFreeChart chart = getChart(pathColor,"","","",false);
Point p = new Point(400,300);
File chartFile = new File(path);
ChartUtils.writeChartAsPNG(new FileOutputStream(chartFile), chart, Double.valueOf(p.getX()).intValue(), Double.valueOf(p.getY()).intValue(),false,0);
} catch (Throwable t) {
t.printStackTrace();
}
return null;
}
private JFreeChart getChart(
String hexColor,
String title,
String category,
String value,
boolean includeLegend) {
TimeSeriesCollection dataset = new TimeSeriesCollection();
TimeSeries series1 = new TimeSeries("Total");
series1.add(new Day(7,10,2019),0);
series1.add(new Day(8,10,2019),6);
series1.add(new Day(9,10,2019),0);
series1.add(new Day(10,10,2019),2);
series1.add(new Day(11,10,2019),0);
dataset.addSeries(series1);
XYSplineRenderer renderer = new XYSplineRenderer();
renderer.setDrawOutlines(true);
renderer.setPrecision(1000);
renderer.setDrawSeriesLineAsPath(true);
renderer.setSeriesPaint(0, Color.decode(hexColor));
renderer.setSeriesShapesVisible(0, true);
renderer.setSeriesItemLabelsVisible(0, true);
renderer.setSeriesItemLabelPaint(0, Color.decode(hexColor));
renderer.setSeriesShape(0, new Ellipse2D.Double(-3, -3, 6, 6));
renderer.setSeriesItemLabelGenerator(0, new XYItemLabelGenerator() {
#Override
public String generateLabel(XYDataset dataset, int series, int item) {
Double d = dataset.getYValue(series, item);
return "" + d.intValue();
}
});
JFreeChart chart = ChartFactory.createTimeSeriesChart(title, category, value, null,includeLegend, true, false);
chart.setAntiAlias(true);
XYPlot plot = (XYPlot)chart.getPlot();
plot.setRangeGridlinePaint(new Color(155, 155, 155) );
plot.setBackgroundPaint(new Color(0, 0, 0, 0));
final TickUnits standardUnits = new TickUnits();
standardUnits.add(
new DateTickUnit(DateTickUnitType.DAY, 1, new SimpleDateFormat("dd E"))
);
DateAxis domainAxis = (DateAxis)plot.getDomainAxis();
domainAxis.setTickLabelFont(new Font("Montserrat",Font.BOLD, 10));
domainAxis.setTickLabelPaint(Color.decode("#9b9b9b"));
domainAxis.setStandardTickUnits(standardUnits);
final NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis();
rangeAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
rangeAxis.setTickLabelFont(new Font("Montserrat",Font.BOLD, 10));
rangeAxis.setTickLabelPaint(Color.decode("#9b9b9b"));
plot.setDataset(0, dataset);
plot.setRenderer(0, renderer);
plot.setOutlinePaint(null);
return chart;
}
}

Related

How to create hollow shapes in JFreeChart Scatter Plot

JFreeChart version used: 1.5.0
I tried the following to obtain hollow shapes in a scatter plot:
PlotFrame.java file content:
package javaapplication1;
import javax.swing.JFrame;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
/**
*
* #author tarik
*/
public class PlotFrame extends javax.swing.JFrame {
public static void main(String[] argv) {
PlotFrame plotFrame = new PlotFrame();
plotFrame.setVisible(true);
}
/**
* Creates new form PlotFrame
*/
public PlotFrame() {
this.setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JFreeChart chart = createChart();
ChartPanel panel = new ChartPanel(chart);
this.add(panel);
pack();
}
private JFreeChart createChart() {
// Create dataset
XYDataset dataset = createDataset();
// Create chart
JFreeChart chart = ChartFactory.createScatterPlot(
"Boys VS Girls weight comparison chart",
"X-Axis", "Y-Axis", dataset);
XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer) chart.getXYPlot().getRenderer();
//renderer.setBaseShapesFilled(false);
renderer.setUseFillPaint(false);
return chart;
}
private XYDataset createDataset() {
XYSeriesCollection dataset = new XYSeriesCollection();
//Boys (Age,weight) series
XYSeries series1 = new XYSeries("Boys");
series1.add(1, 72.9);
series1.add(2, 81.6);
series1.add(3, 88.9);
series1.add(4, 96);
series1.add(5, 102.1);
series1.add(6, 108.5);
series1.add(7, 113.9);
series1.add(8, 119.3);
series1.add(9, 123.8);
series1.add(10, 124.4);
dataset.addSeries(series1);
//Girls (Age,weight) series
XYSeries series2 = new XYSeries("Girls");
series2.add(1, 72.5);
series2.add(2, 80.1);
series2.add(3, 87.2);
series2.add(4, 94.5);
series2.add(5, 101.4);
series2.add(6, 107.4);
series2.add(7, 112.8);
series2.add(8, 118.2);
series2.add(9, 122.9);
series2.add(10, 123.4);
dataset.addSeries(series2);
return dataset;
}
}
It did not work, the shapes are still filled.
Recall that Graphics2D can draw() the outline of a Shape as well as fill() its interior. By limiting rendering to draw(), #micro's approach is sound in this regard, but it assumes version 1.0.19. When migrating to version 1.5 and later, note that "many methods getBaseXXX()/setBaseXXX() have been renamed setDefaultXXX()/getDefaultXXX()." The correct formulation is illustrated below:
renderer.setDefaultShapesFilled(false);
renderer.setUseFillPaint(false);
Other changes:
Construct and manipulate Swing GUI objects only on the event dispatch thread.
Override getPreferredSize() to set the chart's initial preferred size.
As tested:
import java.awt.Dimension;
import java.awt.EventQueue;
import javax.swing.JFrame;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
/**
* #see https://stackoverflow.com/q/58812592/230513
* #author trashgod
* #author tarik
*/
public class PlotFrame extends javax.swing.JFrame {
public static void main(String[] argv) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
PlotFrame plotFrame = new PlotFrame();
plotFrame.setLocationRelativeTo(null);
plotFrame.setVisible(true);
}
});
}
/**
* Creates new form PlotFrame
*/
public PlotFrame() {
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JFreeChart chart = createChart();
ChartPanel panel = new ChartPanel(chart){
#Override
public Dimension getPreferredSize() {
return new Dimension(640, 480);
}
};
this.add(panel);
this.pack();
}
private JFreeChart createChart() {
// Create dataset
XYDataset dataset = createDataset();
// Create chart
JFreeChart chart = ChartFactory.createScatterPlot(
"Boys VS Girls weight comparison chart",
"X-Axis", "Y-Axis", dataset);
XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer) chart.getXYPlot().getRenderer();
renderer.setDefaultShapesFilled(false);
renderer.setUseFillPaint(false);
return chart;
}
private XYDataset createDataset() {
XYSeriesCollection dataset = new XYSeriesCollection();
//Boys (Age,weight) series
XYSeries series1 = new XYSeries("Boys");
series1.add(1, 72.9);
series1.add(2, 81.6);
series1.add(3, 88.9);
series1.add(4, 96);
series1.add(5, 102.1);
series1.add(6, 108.5);
series1.add(7, 113.9);
series1.add(8, 119.3);
series1.add(9, 123.8);
series1.add(10, 124.4);
dataset.addSeries(series1);
//Girls (Age,weight) series
XYSeries series2 = new XYSeries("Girls");
series2.add(1, 72.5);
series2.add(2, 80.1);
series2.add(3, 87.2);
series2.add(4, 94.5);
series2.add(5, 101.4);
series2.add(6, 107.4);
series2.add(7, 112.8);
series2.add(8, 118.2);
series2.add(9, 122.9);
series2.add(10, 123.4);
dataset.addSeries(series2);
return dataset;
}
}
From source code:
if (getItemShapeFilled(series, item)) {
if (this.useFillPaint) {
g2.setPaint(getItemFillPaint(series, item));
}
else {
g2.setPaint(getItemPaint(series, item));
}
g2.fill(shape);
}
For the first if there is no else, so we need the getItemShapeFilled to return false. It looks like this:
public boolean getItemShapeFilled(int series, int item) {
Boolean flag = getSeriesShapesFilled(series);
if (flag != null) {
return flag;
}
return this.baseShapesFilled;
}
Easiest way is to use this method:
public void setBaseShapesFilled(boolean flag) {
this.baseShapesFilled = flag;
fireChangeEvent();
}
So the actual code is:
import javax.swing.JFrame;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
/**
*
* #author tarik
*/
public class PlotFrame extends javax.swing.JFrame {
public static void main(String[] argv) {
PlotFrame plotFrame = new PlotFrame();
plotFrame.setVisible(true);
}
/**
* Creates new form PlotFrame
*/
public PlotFrame() {
this.setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JFreeChart chart = createChart();
ChartPanel panel = new ChartPanel(chart);
this.add(panel);
pack();
}
private JFreeChart createChart() {
// Create dataset
XYDataset dataset = createDataset();
// Create chart
JFreeChart chart = ChartFactory.createScatterPlot(
"Boys VS Girls weight comparison chart",
"X-Axis", "Y-Axis", dataset);
XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer) chart.getXYPlot().getRenderer();
renderer.setBaseShapesFilled(false);
renderer.setUseFillPaint(false);
return chart;
}
private XYDataset createDataset() {
XYSeriesCollection dataset = new XYSeriesCollection();
//Boys (Age,weight) series
XYSeries series1 = new XYSeries("Boys");
series1.add(1, 72.9);
series1.add(2, 81.6);
series1.add(3, 88.9);
series1.add(4, 96);
series1.add(5, 102.1);
series1.add(6, 108.5);
series1.add(7, 113.9);
series1.add(8, 119.3);
series1.add(9, 123.8);
series1.add(10, 124.4);
dataset.addSeries(series1);
//Girls (Age,weight) series
XYSeries series2 = new XYSeries("Girls");
series2.add(1, 72.5);
series2.add(2, 80.1);
series2.add(3, 87.2);
series2.add(4, 94.5);
series2.add(5, 101.4);
series2.add(6, 107.4);
series2.add(7, 112.8);
series2.add(8, 118.2);
series2.add(9, 122.9);
series2.add(10, 123.4);
dataset.addSeries(series2);
return dataset;
}
}
My result is:
Isn't that what you meant?

JFreeChart get getWidth() returns 0.0

I' m using the ChartPanel class from JFreeChart and want to know the width of the plot's data area.
The returned value from the method chartPanel.getChartRenderingInfo().getPlotInfo().getDataArea().getWidth() is 0.0d.
I'm using the JFreeChart version 1.5.0.
My suspicion was a thread-problem, which means that the plotting isn't finished, when I call the .getWidth()-method. So I tried to use ChartChange events, but the result is always a width of 0.0 pixels.
Here is a short example class I created to Show the Problem:
The println in the main() gives "xAxisLengthInPixels = 0.0":
package jFreeChartTest;
import java.awt.BorderLayout;
import javax.swing.JFrame;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.event.ChartChangeEvent;
import org.jfree.chart.event.ChartChangeListener;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
public class Panel_Chart extends JFrame implements ChartChangeListener {
private ChartPanel chartPanel = null;
private int width = 1024;
private int height = 820;
public Panel_Chart() {
this.setTitle("XY Plot");
setBounds(0, 0, width, height);
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
XYSeriesCollection xySeriesCollection = new XYSeriesCollection();
XYSeries testXYSeries = new XYSeries("Test-Series", true);
testXYSeries.add(4,10);
testXYSeries.add(5,20);
testXYSeries.add(6,30);
xySeriesCollection.addSeries(testXYSeries);
boolean includeLegend = true;
boolean useTooltips = true;
boolean useUrls = true;
JFreeChart chart = ChartFactory.createScatterPlot(//createXYLineChart(
"",
"X-Axis",
"Y-Axis",
xySeriesCollection,
PlotOrientation.VERTICAL,
includeLegend,
useTooltips,
useUrls);
this.chartPanel = new ChartPanel(chart);
getContentPane().add(chartPanel, BorderLayout.PAGE_START);
}
public Double getXAxisLengthInPixels(){
return chartPanel.getChartRenderingInfo().getPlotInfo().getDataArea().getWidth();
}
#Override
public void chartChanged(ChartChangeEvent arg0) {
// TODO Auto-generated method stub
System.out.println("ChartChangeEvent:" + arg0);
}
public static void main(String[] args){
Panel_Chart panelChart = new Panel_Chart();
panelChart.setVisible(true);
System.out.println("xAxisLengthInPixels = " + panelChart.getXAxisLengthInPixels());
}
}

JavaFX How can I make a line chart out of an observable list?

I have been playing around with the line chart tutorial here: http://docs.oracle.com/javase/8/javafx/user-interface-tutorial/line-chart.htm#CIHGBCFI
I want to extend the tutorial and attempt to build a line chart out of data from a database, instead of setting data like this:
series.getData().add(new XYChart.Data(1, 23));
series.getData().add(new XYChart.Data(2, 14));
What I have to import data from database:
public ObservableList<Items> loadChart() {
try {
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery("SELECT cost, date FROM Items ORDER BY date ASC");
ObservableList<Items> data = FXCollections.observableArrayList();
while(rs.next()){
Items items = new Items();
items.setCost(rs.getInt(1));
items.setDate(rs.getString(2));
data.add(items);
}
return data;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
I want to plot cost and date against in a line chart... Yet don't know how to do so (based on observable list and the tutorial).
If you are using that sample, your code should look something like this.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.stage.Stage;
public class LineChartSample extends Application {
#Override public void start(Stage stage) {
stage.setTitle("Line Chart Sample");
//defining the axes
final NumberAxis xAxis = new NumberAxis();
final NumberAxis yAxis = new NumberAxis();
xAxis.setLabel("Number of Month");
//creating the chart
final LineChart<Number,Number> lineChart =
new LineChart<Number,Number>(xAxis,yAxis);
lineChart.setTitle("Stock Monitoring, 2010");
//defining a series
XYChart.Series series = new XYChart.Series();
series.setName("My portfolio");
//populating the series with data
try
{
Connection connection = DriverManager.getConnection("...");//You can use try with resources. Establish a Connection
Statement stmt = connection.createStatement();//Create Statement
ResultSet rs = stmt.executeQuery("SELECT cost, date FROM Items ORDER BY date ASC");//Query DB and get results.
//Iterate through results.
while(rs.next())
{
series.getData().add(new XYChart.Data(rs.getInt(1), Integer.parseInt(rs.getString(2))));//Add data to Chart. Changed the second input to Integer due to LineChart<Number,Number>. This should work, though I haven't tested it.
}
}
catch (SQLException ex) {
Logger.getLogger(LineChartSample.class.getName()).log(Level.SEVERE, null, ex);
}
Scene scene = new Scene(lineChart,800,600);
lineChart.getData().add(series);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
UPDATE BELOW
You can try something like this if you want to have a database handler class.
DBHandler Class
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.scene.chart.XYChart;
/**
*
* #author blj0011
*/
public class DBHandler
{
List<List<Integer>> dataHolder;
//There is probably a better way to structure this DBHandler Class
public DBHandler()
{
dataHolder = new ArrayList();
try
{
Connection connection = DriverManager.getConnection("...");//You can use try with resources. Establish a Connection
Statement stmt = connection.createStatement();//Create Statement
ResultSet rs = stmt.executeQuery("SELECT cost, date FROM Items ORDER BY date ASC");//Query DB and get results.
//Iterate through results.
while(rs.next())
{
List<Integer> tempDataHolder = new ArrayList();
tempDataHolder.add(rs.getInt(1));
tempDataHolder.add(Integer.parseInt(rs.getString(2)));
dataHolder.add(tempDataHolder);
}
}
catch (SQLException ex) {
Logger.getLogger(DBHandler.class.getName()).log(Level.SEVERE, null, ex);
}
}
public List<List<Integer>> getDataHolder()
{
return dataHolder;
}
}
Main Class
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.stage.Stage;
public class LineChartSample extends Application {
#Override public void start(Stage stage) {
stage.setTitle("Line Chart Sample");
//defining the axes
final NumberAxis xAxis = new NumberAxis();
final NumberAxis yAxis = new NumberAxis();
xAxis.setLabel("Number of Month");
//creating the chart
final LineChart<Number,Number> lineChart =
new LineChart<Number,Number>(xAxis,yAxis);
lineChart.setTitle("Stock Monitoring, 2010");
//defining a series
XYChart.Series series = new XYChart.Series();
series.setName("My portfolio");
//populating the series with data
DBHandler dbHandler = new DBHandler();
List<List<Integer>> dataHolder = dbHandler.getDataHolder();
for(int i = 0; i < dataHolder.size(); i++)
{
series.getData().add(new XYChart.Data(dataHolder.get(i).get(0), dataHolder.get(i).get(1)));
}
Scene scene = new Scene(lineChart,800,600);
lineChart.getData().add(series);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}

XYDataset with a single data point : nothing gets plotted

This is a corner case I encountered. Below's a SSCCE:
import java.util.*;
import java.io.*;
import java.awt.Color;
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
import org.jfree.chart.*;
import org.jfree.chart.plot.*;
import org.jfree.chart.axis.*;
import org.jfree.data.time.*;
import org.jfree.data.xy.XYDataset;
public class FooMain {
public static void main(String args[]) throws Exception {
BufferedImage img = timeAxisSSEProcessionsChart();
ImageIO.write(img, "png", new File("img.png"));
System.exit(0);
}
private static XYDataset createTimeSeriesDataset() {
TimeSeries timeSeries = new TimeSeries("series-a");
timeSeries.add(RegularTimePeriod.createInstance(Millisecond.class, new java.util.Date(0) , TimeZone.getTimeZone("Zulu")), 100);
// if below line is commented out, nothing is plotted:
timeSeries.add(RegularTimePeriod.createInstance(Millisecond.class, new java.util.Date(1000), TimeZone.getTimeZone("Zulu")), 100);
TimeSeriesCollection rv = new TimeSeriesCollection();
rv.addSeries(timeSeries);
return rv;
}
public static BufferedImage timeAxisSSEProcessionsChart() throws Exception {
XYDataset dataset = createTimeSeriesDataset();
JFreeChart chart = ChartFactory.createTimeSeriesChart("title", "date", "series-a", dataset, true, true, false);
InputStream in = new ByteArrayInputStream(imageBytesFromChart(chart, 600, 400));
return ImageIO.read(in);
}
private static byte[] imageBytesFromChart(JFreeChart chart, int width, int height) {
BufferedImage objBufferedImage = chart.createBufferedImage(width, height);
ByteArrayOutputStream bas = new ByteArrayOutputStream();
try {
ImageIO.write(objBufferedImage, "png", bas);
} catch (IOException e) {
e.printStackTrace();
}
byte[] byteArray=bas.toByteArray();
return byteArray;
}
}
The above code produces a plot as expected.
If however we comment out the line indicated in the createTimeSeriesDataset method, then nothing gets plotted:
So the question is: how can I ensure that at least a dot (or some other mark) gets printed in the corner case where the XYDataset contains only one data point?
The time series chart primarily consists of lines connecting the individual data points. When there is only one data element, there are no data points to connect. So far, so obvious.
One possible solution would be to enable the "tick shapes" for the chart when there is only one entry. I'm not sure whether this is an appropriate solution for your case. But this could be done with a method like
private static void showTickMarksForSingleElements(
XYDataset dataset, JFreeChart chart)
{
TimeSeriesCollection timeSeriesCollection =
(TimeSeriesCollection)dataset;
List<?> series = timeSeriesCollection.getSeries();
TimeSeries timeSeries = (TimeSeries) series.get(0);
if (timeSeries.getItemCount() == 1)
{
XYPlot plot = (XYPlot) chart.getPlot();
XYLineAndShapeRenderer renderer
= (XYLineAndShapeRenderer) plot.getRenderer();
renderer.setSeriesShapesVisible(0, true);
}
}
(Caution, there are obviously some assumptions about the types of the given objects involved - in doubt, check the types before doing the casts!)
The result would then be a single tick mark for time series that only contain a single element:
Here as another MVCE:
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.TimeZone;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.time.Millisecond;
import org.jfree.data.time.RegularTimePeriod;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;
import org.jfree.data.xy.XYDataset;
public class SingleElementChart {
public static void main(String args[]) throws Exception {
BufferedImage img = timeAxisSSEProcessionsChart();
show(img);
}
private static void show(final BufferedImage img)
{
SwingUtilities.invokeLater(new Runnable()
{
#Override
public void run()
{
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.getContentPane().add(new JLabel(new ImageIcon(img)));
f.pack();
f.setLocationRelativeTo(null);
f.setVisible(true);
}
});
}
private static TimeSeriesCollection createTimeSeriesDataset() {
TimeSeries timeSeries = new TimeSeries("series-a");
timeSeries.add(RegularTimePeriod.createInstance(
Millisecond.class, new java.util.Date(0),
TimeZone.getTimeZone("Zulu")), 100);
// if below line is commented out, nothing is plotted:
//timeSeries.add(RegularTimePeriod.createInstance(
// Millisecond.class, new java.util.Date(1000),
// TimeZone.getTimeZone("Zulu")), 100);
TimeSeriesCollection rv = new TimeSeriesCollection();
rv.addSeries(timeSeries);
return rv;
}
public static BufferedImage timeAxisSSEProcessionsChart() throws Exception {
XYDataset dataset = createTimeSeriesDataset();
JFreeChart chart = ChartFactory.createTimeSeriesChart(
"title", "date", "series-a", dataset, true, true, false);
showTickMarksForSingleElements(dataset, chart);
InputStream in = new ByteArrayInputStream(
imageBytesFromChart(chart, 600, 400));
return ImageIO.read(in);
}
private static void showTickMarksForSingleElements(
XYDataset dataset, JFreeChart chart)
{
TimeSeriesCollection timeSeriesCollection =
(TimeSeriesCollection)dataset;
List<?> series = timeSeriesCollection.getSeries();
TimeSeries timeSeries = (TimeSeries) series.get(0);
if (timeSeries.getItemCount() == 1)
{
XYPlot plot = (XYPlot) chart.getPlot();
XYLineAndShapeRenderer renderer
= (XYLineAndShapeRenderer) plot.getRenderer();
renderer.setSeriesShapesVisible(0, true);
}
}
private static byte[] imageBytesFromChart(JFreeChart chart, int width, int height) {
BufferedImage objBufferedImage =
chart.createBufferedImage(width, height);
ByteArrayOutputStream bas = new ByteArrayOutputStream();
try {
ImageIO.write(objBufferedImage, "png", bas);
} catch (IOException e) {
e.printStackTrace();
}
byte[] byteArray=bas.toByteArray();
return byteArray;
}

JFreeChart MouseListener doesn't resolve chart elements

I've got stacked bar chart where I want to be able to select individual bars in the stack. But ChartMouseListener doesn't resolve ChartMouseEvent into corresponding ChartEntity. Here's the listener snippet :
public void chartMouseClicked(ChartMouseEvent event){
ChartEntity entity = event.getEntity();
if(entity != null && (entity instanceof XYItemEntity) ){
XYItemEntity item = (XYItemEntity)entity;
renderer.select(item.getSeriesIndex(), item.getItem());
return;
}
// deselect
renderer.select(-1,-1);
}
The problem is that event.getEntity() returns null when I am obviously clicking on some of the bars. Note that NOT all the bars fail. The further I go to the right end of the chart, the more obvious is the shift in coordinates. Snap shot below showing that selected bar actually appears when clicking outside of it. I am using JFreeChart within SWT composite. Can anyone confirm that this is a buggy behavior or is there a workaround?
Below is complete sscce, after you run it and click on bars - it will show up pinky. Then re-size the window and try to select bars - it will miss. And I think the miss is the function of the new size.
import java.awt.Color;
import java.awt.Paint;
import java.util.Random;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.jfree.chart.ChartMouseEvent;
import org.jfree.chart.ChartMouseListener;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.DateAxis;
import org.jfree.chart.axis.DateTickMarkPosition;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.entity.ChartEntity;
import org.jfree.chart.entity.XYItemEntity;
import org.jfree.chart.event.RendererChangeEvent;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.StackedXYBarRenderer;
import org.jfree.chart.renderer.xy.StandardXYBarPainter;
import org.jfree.data.time.Day;
import org.jfree.data.time.Hour;
import org.jfree.data.time.TimeTableXYDataset;
import org.jfree.data.xy.TableXYDataset;
import org.jfree.experimental.chart.swt.ChartComposite;
public class StackedChartSwt {
private StackedRenderer renderer;
private Color[] colors = new Color[]{
new Color(230,240,255),
new Color(240,255,240),
new Color(255,255,255),
new Color(255,255,240),
new Color(255,240,240),
new Color(240,240,240)
};
public StackedChartSwt(){
Display display = new Display();
final Shell shell = new Shell(display);
shell.setLayout(new FillLayout());
JFreeChart chart = createStackedChart(createStackedDataset());
ChartComposite chartComposite = new ChartComposite(shell, SWT.NONE, chart, false, false, false, false, false);
chartComposite.setLayoutData(new GridData(GridData.FILL_BOTH));
chartComposite.setRangeZoomable(false);
chartComposite.setMenu(null);
chartComposite.addChartMouseListener(new ThisMouseListener());
shell.setSize(800, 600);
shell.open();
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
display.dispose();
}
private JFreeChart createStackedChart(TableXYDataset tablexydataset) {
DateAxis dateaxis = new DateAxis();
dateaxis.setTickMarkPosition(DateTickMarkPosition.MIDDLE);
NumberAxis numberaxis = new NumberAxis("Event counts");
renderer = new StackedRenderer();
XYPlot plot = new XYPlot(tablexydataset, dateaxis, numberaxis, renderer);
plot.setBackgroundPaint(Color.white);
plot.setDomainGridlinePaint(Color.lightGray);
plot.setDomainGridlinesVisible(true);
plot.setRangeGridlinesVisible(true);
plot.setRangeGridlinePaint(Color.lightGray);
JFreeChart chart = new JFreeChart(null, plot);
chart.setBackgroundPaint(Color.white);
chart.setBorderVisible(false);
chart.setBorderPaint(null);
return chart;
}
class StackedRenderer extends StackedXYBarRenderer{
int selectedRow=-1, selectedCol=-1;
public StackedRenderer(){
setDrawBarOutline(true);
setBarPainter(new StandardXYBarPainter());
setShadowVisible(false);
setSeriesPaint(0, Color.blue);
setMargin(0.2);
}
public void select(int row, int col){
selectedRow = row;
selectedCol = col;
notifyListeners(new RendererChangeEvent(this));
}
#Override
public Paint getItemPaint(final int row, final int col){
if(row == selectedRow && col == selectedCol)
return Color.pink;
return colors[row];
}
}
class ThisMouseListener implements ChartMouseListener{
public void chartMouseMoved(ChartMouseEvent event){
}
public void chartMouseClicked(ChartMouseEvent event){
ChartEntity entity = event.getEntity();
if(entity != null && (entity instanceof XYItemEntity) ){
XYItemEntity item = (XYItemEntity)entity;
renderer.select(item.getSeriesIndex(), item.getItem());
return;
}
// deselect
renderer.select(-1,-1);
}
}
private TableXYDataset createStackedDataset(){
Random random = new Random(0);
TimeTableXYDataset ds = new TimeTableXYDataset();
Day day = new Day();
for( int i = 0; i < 24; i++ ){
Hour hour = new Hour( i, day );
ds.add(hour, random.nextInt( 20 ), "A");
ds.add(hour, random.nextInt( 20 ), "B");
ds.add(hour, random.nextInt( 20 ), "C");
ds.add(hour, random.nextInt( 20 ), "D");
ds.add(hour, random.nextInt( 20 ), "E");
ds.add(hour, random.nextInt( 20 ), "F");
}
return ds;
}
public static void main(String[] args){
new StackedChartSwt();
}
}
Your exemplary renderer has the correct geometry when run from Swing, as shown below. I'm unsure why things are awry with SWT, but this result may narrow the search.
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Paint;
import java.util.Random;
import javax.swing.JFrame;
import org.jfree.chart.ChartMouseEvent;
import org.jfree.chart.ChartMouseListener;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.DateAxis;
import org.jfree.chart.axis.DateTickMarkPosition;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.entity.ChartEntity;
import org.jfree.chart.entity.XYItemEntity;
import org.jfree.chart.event.RendererChangeEvent;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.StackedXYBarRenderer;
import org.jfree.chart.renderer.xy.StandardXYBarPainter;
import org.jfree.data.time.Day;
import org.jfree.data.time.Hour;
import org.jfree.data.time.TimeTableXYDataset;
import org.jfree.data.xy.TableXYDataset;
public class StackedChartSwing {
private ChartPanel panel;
private StackedRenderer renderer;
private Color[] colors = new Color[]{
new Color(230, 240, 255),
new Color(240, 255, 240),
new Color(255, 255, 255),
new Color(255, 255, 240),
new Color(255, 240, 240),
new Color(240, 240, 240)
};
public StackedChartSwing() {
JFreeChart chart = createStackedChart(createStackedDataset());
panel = new ChartPanel(chart);
panel.addChartMouseListener(new ThisMouseListener());
}
private JFreeChart createStackedChart(TableXYDataset tablexydataset) {
DateAxis dateaxis = new DateAxis();
dateaxis.setTickMarkPosition(DateTickMarkPosition.MIDDLE);
NumberAxis numberaxis = new NumberAxis("Event counts");
renderer = new StackedRenderer();
XYPlot plot = new XYPlot(tablexydataset, dateaxis, numberaxis, renderer);
plot.setBackgroundPaint(Color.white);
plot.setDomainGridlinePaint(Color.lightGray);
plot.setDomainGridlinesVisible(true);
plot.setRangeGridlinesVisible(true);
plot.setRangeGridlinePaint(Color.lightGray);
JFreeChart chart = new JFreeChart(null, plot);
chart.setBackgroundPaint(Color.white);
chart.setBorderVisible(false);
chart.setBorderPaint(null);
return chart;
}
class StackedRenderer extends StackedXYBarRenderer {
int selectedRow = -1, selectedCol = -1;
public StackedRenderer() {
setDrawBarOutline(true);
setBarPainter(new StandardXYBarPainter());
setShadowVisible(false);
setSeriesPaint(0, Color.blue);
setMargin(0.2);
}
public void select(int row, int col) {
selectedRow = row;
selectedCol = col;
notifyListeners(new RendererChangeEvent(this));
}
#Override
public Paint getItemPaint(final int row, final int col) {
if (row == selectedRow && col == selectedCol) {
return Color.pink;
}
return colors[row];
}
}
class ThisMouseListener implements ChartMouseListener {
#Override
public void chartMouseMoved(ChartMouseEvent event) {
}
#Override
public void chartMouseClicked(ChartMouseEvent event) {
ChartEntity entity = event.getEntity();
if (entity != null && (entity instanceof XYItemEntity)) {
XYItemEntity item = (XYItemEntity) entity;
renderer.select(item.getSeriesIndex(), item.getItem());
return;
}
// deselect
renderer.select(-1, -1);
}
}
private TableXYDataset createStackedDataset() {
Random random = new Random(0);
TimeTableXYDataset ds = new TimeTableXYDataset();
Day day = new Day();
for (int i = 0; i < 24; i++) {
Hour hour = new Hour(i, day);
ds.add(hour, random.nextInt(20), "A");
ds.add(hour, random.nextInt(20), "B");
ds.add(hour, random.nextInt(20), "C");
ds.add(hour, random.nextInt(20), "D");
ds.add(hour, random.nextInt(20), "E");
ds.add(hour, random.nextInt(20), "F");
}
return ds;
}
private void display() {
JFrame f = new JFrame("Test");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(panel);
f.pack();
f.setLocationRelativeTo(null);
f.setVisible(true);
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
new StackedChartSwing().display();
}
});
}
}
OK, I've found the problem. In SWT there are several constructors to create ChartComposite, most of the use defaults which specify min/max width and height. In my case, whenever I've enlarged the chart above DEFAULT_MAXIMUM_DRAW_WIDTH (800px) - the coordinates start to get messy. So, the solution is to use full constructor and specify relevant boundaries for your display. It was a tough one to crack... mainly due to lack of proper documentation.

Resources