Java tutorial
/* * Copyright (c) 2009, SQL Power Group Inc. * * This file is part of Wabit. * * Wabit is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * Wabit is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package ca.sqlpower.wabit.swingui.chart; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Font; import java.awt.GradientPaint; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import org.apache.log4j.Logger; import org.jfree.chart.ChartFactory; import org.jfree.chart.JFreeChart; import org.jfree.chart.StandardChartTheme; import org.jfree.chart.axis.CategoryAxis; import org.jfree.chart.axis.CategoryLabelPositions; import org.jfree.chart.axis.DateAxis; import org.jfree.chart.axis.ValueAxis; import org.jfree.chart.labels.PieToolTipGenerator; import org.jfree.chart.labels.StandardPieToolTipGenerator; import org.jfree.chart.plot.CategoryPlot; import org.jfree.chart.plot.MultiplePiePlot; import org.jfree.chart.plot.PieLabelLinkStyle; import org.jfree.chart.plot.PiePlot; import org.jfree.chart.plot.PiePlot3DGradient; import org.jfree.chart.plot.Plot; import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.category.BarRenderer; import org.jfree.chart.renderer.category.CategoryItemRenderer; import org.jfree.chart.renderer.category.LineAndShapeRenderer; import org.jfree.chart.renderer.category.StandardBarPainter; import org.jfree.chart.title.LegendTitle; import org.jfree.chart.title.TextTitle; import org.jfree.chart.urls.PieURLGenerator; import org.jfree.chart.urls.StandardPieURLGenerator; import org.jfree.data.category.CategoryDataset; import org.jfree.data.time.TimePeriodValuesCollection; import org.jfree.data.xy.XYDataset; import org.jfree.ui.GradientPaintTransformType; import org.jfree.ui.RectangleEdge; import org.jfree.ui.RectangleInsets; import org.jfree.ui.StandardGradientPaintTransformer; import org.jfree.util.TableOrder; import ca.sqlpower.wabit.report.chart.Chart; import ca.sqlpower.wabit.report.chart.ChartColumn; import ca.sqlpower.wabit.report.chart.ChartType; import ca.sqlpower.wabit.report.chart.ColumnRole; import ca.sqlpower.wabit.report.chart.DatasetType; import ca.sqlpower.wabit.report.chart.LegendPosition; import ca.sqlpower.wabit.rs.olap.QueryInitializationException; /** * This is a collection of swing specific chart utilities. You should not * make an instance of this class. */ public class ChartSwingUtil { private static final Logger logger = Logger.getLogger(ChartSwingUtil.class); /** * This is a collection of swing specific chart utilities. You should not * make an instance of this class. */ private ChartSwingUtil() { /* don't */} /** * The stroke that gets used for horizontal and vertical grid lines in all * charts that need them. */ private static final BasicStroke GRIDLINE_STROKE = new BasicStroke(.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0f, new float[] { .5f, 7f }, 0f); /** * Creates a JFreeChart based on the current query results produced by the * given chart. * * @param c * The chart from which to produce a JFreeChart component. Must * not be null. * @return A chart based on the data and settings in the given chart, or * null if the given chart is not sufficiently configured (for * example, if its type is not set) or it is currently unable to * produce a result set. */ public static JFreeChart createChartFromQuery(Chart c) throws SQLException, QueryInitializationException, InterruptedException { logger.debug("Creating JFreeChart for Wabit chart " + c); ChartType chartType = c.getType(); if (chartType == null) { logger.debug("Returning null (chart's type is not set)"); return null; } final JFreeChart chart; if (chartType.getDatasetType().equals(DatasetType.CATEGORY)) { JFreeChart categoryChart = createCategoryChart(c); logger.debug("Made a new category chart: " + categoryChart); if (categoryChart != null && categoryChart.getPlot() instanceof CategoryPlot) { double rotationRads = Math.toRadians(c.getXAxisLabelRotation()); CategoryLabelPositions clp; if (Math.abs(rotationRads) < 0.05) { clp = CategoryLabelPositions.STANDARD; } else if (rotationRads < 0) { clp = CategoryLabelPositions.createUpRotationLabelPositions(-rotationRads); } else { clp = CategoryLabelPositions.createDownRotationLabelPositions(rotationRads); } CategoryAxis domainAxis = categoryChart.getCategoryPlot().getDomainAxis(); domainAxis.setMaximumCategoryLabelLines(5); domainAxis.setCategoryLabelPositions(clp); } chart = categoryChart; } else if (chartType.getDatasetType().equals(DatasetType.XY)) { JFreeChart xyChart = createXYChart(c); logger.debug("Made a new XY chart: " + xyChart); chart = xyChart; } else { throw new IllegalStateException("Unknown chart dataset type " + chartType.getDatasetType()); } if (chart != null) { makeChartNice(chart); } return chart; } /** * Sets the colours and gradients to be used when painting the given JFreeChart. * * @param chart * The JFreeChart to make nice. */ public static void makeChartNice(JFreeChart chart) { Plot plot = chart.getPlot(); chart.setBackgroundPaint(null); chart.setBorderStroke(new BasicStroke(1f)); chart.setBorderPaint(new Color(0xDDDDDD)); chart.setBorderVisible(true); // TODO Should we add an option for subtitles, this is where it would go. // TextTitle subTitle = new TextTitle("What's up doc?", // new Font("SansSerif", Font.BOLD, 8)); // chart.addSubtitle(subTitle); // overall plot plot.setOutlinePaint(null); plot.setInsets(new RectangleInsets(0, 5, 0, 5)); // also the overall chart panel plot.setBackgroundPaint(null); plot.setDrawingSupplier(new WabitDrawingSupplier()); // legend LegendTitle legend = chart.getLegend(); if (legend != null) { legend.setBorder(0, 0, 0, 0); legend.setBackgroundPaint(null); legend.setPadding(2, 2, 2, 2); } if (plot instanceof CategoryPlot) { CategoryPlot cplot = (CategoryPlot) plot; CategoryItemRenderer renderer = cplot.getRenderer(); if (renderer instanceof BarRenderer) { BarRenderer brenderer = (BarRenderer) renderer; brenderer.setBarPainter(new StandardBarPainter()); brenderer.setDrawBarOutline(false); brenderer.setShadowVisible(false); brenderer.setGradientPaintTransformer( new StandardGradientPaintTransformer(GradientPaintTransformType.HORIZONTAL)); } else if (renderer instanceof LineAndShapeRenderer) { // it's all taken care of by WabitDrawingSupplier } else { logger.warn("I don't know how to make " + renderer + " pretty. Leaving ugly."); } cplot.setRangeGridlinePaint(Color.BLACK); cplot.setRangeGridlineStroke(GRIDLINE_STROKE); // axes for (int i = 0; i < cplot.getDomainAxisCount(); i++) { CategoryAxis axis = cplot.getDomainAxis(i); axis.setAxisLineVisible(false); } for (int i = 0; i < cplot.getRangeAxisCount(); i++) { ValueAxis axis = cplot.getRangeAxis(i); axis.setAxisLineVisible(false); } } if (plot instanceof MultiplePiePlot) { MultiplePiePlot mpplot = (MultiplePiePlot) plot; JFreeChart pchart = mpplot.getPieChart(); PiePlot3DGradient pplot = (PiePlot3DGradient) pchart.getPlot(); pplot.setBackgroundPaint(null); pplot.setOutlinePaint(null); pplot.setFaceGradientPaintTransformer( new StandardGradientPaintTransformer(GradientPaintTransformType.HORIZONTAL)); pplot.setSideGradientPaintTransformer( new StandardGradientPaintTransformer(GradientPaintTransformType.HORIZONTAL)); CategoryDataset data = mpplot.getDataset(); Color[][] colours = WabitDrawingSupplier.SERIES_COLOURS; //Set all colours for (int i = 0; i < colours.length; i++) { if (data.getColumnCount() >= i + 1) { pplot.setSectionOutlinePaint(data.getColumnKey(i), null); GradientPaint gradient = new GradientPaint(0, 0f, colours[i][0], 100, 0f, colours[i][1]); pplot.setSectionPaint(data.getColumnKey(i), gradient); gradient = new GradientPaint(0, 0f, colours[i][1], 100, 0f, colours[i][0]); pplot.setSidePaint(data.getColumnKey(i), gradient); } } } // Tweak the title font size chart.getTitle().setFont(chart.getTitle().getFont().deriveFont(14f)); chart.getTitle().setPadding(5, 0, 5, 0); chart.setAntiAlias(true); // shrink padding chart.setPadding(new RectangleInsets(0, 0, 0, 0)); } /** * Creates a JFreeChart based on the given query results and how the columns * are defined in the chart. * * @param c * The chart from which to extract the data set and configure the * JFreeChart instance * @return A chart based on the data in the query of the given type, or null * if any of the following conditions hold: * <ul> * <li>The chart is currently unable to produce a result set * <li>The chart does not have at least one category and one series * column defined * </ul> */ private static JFreeChart createCategoryChart(Chart c) { if (c.getType().getDatasetType() != DatasetType.CATEGORY) { throw new IllegalStateException( "Chart is not currently set up as a category chart " + "(it is a " + c.getType() + ")"); } List<ChartColumn> columnNamesInOrder = c.getColumns(); LegendPosition legendPosition = c.getLegendPosition(); String chartName = c.getName(); String yaxisName = c.getYaxisName(); String xaxisName = c.getXaxisName(); boolean containsCategory = false; boolean containsSeries = false; for (ChartColumn col : columnNamesInOrder) { if (col.getRoleInChart().equals(ColumnRole.CATEGORY)) { containsCategory = true; } else if (col.getRoleInChart().equals(ColumnRole.SERIES)) { containsSeries = true; } } if (!containsCategory || !containsSeries) { return null; } List<ChartColumn> categoryColumns = c.findRoleColumns(ColumnRole.CATEGORY); List<String> categoryColumnNames = new ArrayList<String>(); for (ChartColumn identifier : categoryColumns) { categoryColumnNames.add(identifier.getName()); } // because of the chart type check at the beginning of this method, the // following cast should always work CategoryDataset dataset = (CategoryDataset) c.createDataset(); return createCategoryChartFromDataset(c, dataset, c.getType(), legendPosition, chartName, yaxisName, xaxisName); } /** * This is a helper method for creating charts and is split off as it does * only the chart creation and tweaking of a category chart. All of the * decisions for what columns are defined as what and how the data is stored * should be done in the method that calls this. * <p> * Given a dataset and other chart properties this will create an * appropriate JFreeChart. * @param c * * @param dataset * The data to create a chart from. * @param chartType * The type of chart to create for the category dataset. * @param legendPosition * The position where the legend should appear. * @param chartName * The title of the chart. * @param yaxisName * The title of the Y axis. * @param xaxisName * The title of the X axis. * @return A JFreeChart that represents the dataset given. */ private static JFreeChart createCategoryChartFromDataset(Chart c, CategoryDataset dataset, ChartType chartType, LegendPosition legendPosition, String chartName, String yaxisName, String xaxisName) { if (chartType == null || dataset == null) { return null; } boolean showLegend = !legendPosition.equals(LegendPosition.NONE); JFreeChart chart; ChartFactory.setChartTheme(StandardChartTheme.createLegacyTheme()); BarRenderer.setDefaultBarPainter(new StandardBarPainter()); if (chartType == ChartType.BAR) { chart = ChartFactory.createBarChart(chartName, xaxisName, yaxisName, dataset, PlotOrientation.VERTICAL, showLegend, true, false); } else if (chartType == ChartType.PIE) { chart = createPieChart(chartName, dataset, TableOrder.BY_ROW, showLegend, true, false); } else if (chartType == ChartType.CATEGORY_LINE) { chart = ChartFactory.createLineChart(chartName, xaxisName, yaxisName, dataset, PlotOrientation.VERTICAL, showLegend, true, false); } else { throw new IllegalArgumentException("Unknown chart type " + chartType + " for a category dataset."); } if (chart == null) return null; if (legendPosition != LegendPosition.NONE) { chart.getLegend().setPosition(legendPosition.getRectangleEdge()); //chart.getTitle().setPadding(4,4,15,4); } if (chart.getPlot() instanceof MultiplePiePlot) { MultiplePiePlot mplot = (MultiplePiePlot) chart.getPlot(); PiePlot plot = (PiePlot) mplot.getPieChart().getPlot(); plot.setLabelLinkStyle(PieLabelLinkStyle.CUBIC_CURVE); if (showLegend) { // for now, legend and items labels are mutually exclusive. Could make this a user pref. plot.setLabelGenerator(null); } } if (!c.isAutoYAxisRange()) { CategoryPlot plot = chart.getCategoryPlot(); ValueAxis axis = plot.getRangeAxis(); axis.setAutoRange(false); axis.setRange(c.getYAxisMinRange(), c.getYAxisMaxRange()); } return chart; } /** * Creates a chart that displays multiple 3D pie plots that have a GradientPaintTransformer. * The chart object returned by this method uses a {@link MultiplePiePlot} instance as the plot. * * @param title the chart title (<code>null</code> permitted). * @param dataset the dataset (<code>null</code> permitted). * @param order the order that the data is extracted (by row or by column) * (<code>null</code> not permitted). * @param legend include a legend? * @param tooltips generate tooltips? * @param urls generate URLs? * * @return A chart. */ private static JFreeChart createPieChart(String title, CategoryDataset dataset, TableOrder order, boolean legend, boolean tooltips, boolean urls) { if (order == null) { throw new IllegalArgumentException("Null 'order' argument."); } MultiplePiePlot plot = new MultiplePiePlot(dataset); plot.setDataExtractOrder(order); plot.setBackgroundPaint(null); plot.setOutlineStroke(null); JFreeChart pieChart = new JFreeChart(new PiePlot3DGradient(null)); TextTitle seriesTitle = new TextTitle("Series Title", new Font("SansSerif", Font.BOLD, 10)); seriesTitle.setPosition(RectangleEdge.BOTTOM); pieChart.setTitle(seriesTitle); pieChart.getTitle().setPadding(0, 0, 0, 0); pieChart.removeLegend(); pieChart.setBackgroundPaint(null); pieChart.setPadding(new RectangleInsets(0, 0, 0, 0)); pieChart.getPlot().setInsets(new RectangleInsets(0, 0, 0, 0)); plot.setPieChart(pieChart); if (tooltips) { PieToolTipGenerator tooltipGenerator = new StandardPieToolTipGenerator(); PiePlot pp = (PiePlot) plot.getPieChart().getPlot(); pp.setToolTipGenerator(tooltipGenerator); } if (urls) { PieURLGenerator urlGenerator = new StandardPieURLGenerator(); PiePlot pp = (PiePlot) plot.getPieChart().getPlot(); pp.setURLGenerator(urlGenerator); } JFreeChart chart = new JFreeChart(title, JFreeChart.DEFAULT_TITLE_FONT, plot, legend); return chart; } /** * Creates a chart based on the data in the given query. * * @param c * The chart to extract the dataset and JFreeChart settings from. * @return A chart based on the data in the query of the given type. */ private static JFreeChart createXYChart(Chart c) { if (c.getType().getDatasetType() != DatasetType.XY) { throw new IllegalStateException( "Chart is not currently set up as an XY chart " + "(it is a " + c.getType() + ")"); } List<ChartColumn> columnNamesInOrder = c.getColumns(); LegendPosition legendPosition = c.getLegendPosition(); String chartName = c.getName(); String yaxisName = c.getYaxisName(); String xaxisName = c.getXaxisName(); final XYDataset xyCollection = (XYDataset) c.createDataset(); boolean containsSeries = false; for (ChartColumn identifier : columnNamesInOrder) { if (identifier.getRoleInChart().equals(ColumnRole.SERIES)) { containsSeries = true; break; } } if (!containsSeries) { return null; } if (xyCollection == null) { return null; } return createChartFromXYDataset(c, xyCollection, c.getType(), legendPosition, chartName, yaxisName, xaxisName); } /** * This is a helper method for creating a line chart. This should only do * the chart creation and not setting up the dataset. The calling method * should do the logic for the dataset setup. * @param c * * @param xyCollection * The dataset to display a chart for. * @param chartType * The chart type. This must be a valid chart type that can be * created from an XY dataset. At current only line and scatter * are supported * @param legendPosition * The position of the legend. * @param chartName * The name of the chart. * @param yaxisName * The name of the y axis. * @param xaxisName * The name of the x axis. * @return A chart of the specified chartType based on the given dataset. */ private static JFreeChart createChartFromXYDataset(Chart c, XYDataset xyCollection, ChartType chartType, LegendPosition legendPosition, String chartName, String yaxisName, String xaxisName) { boolean showLegend = !legendPosition.equals(LegendPosition.NONE); JFreeChart chart; if (chartType.equals(ChartType.LINE)) { chart = ChartFactory.createXYLineChart(chartName, xaxisName, yaxisName, xyCollection, PlotOrientation.VERTICAL, showLegend, true, false); } else if (chartType.equals(ChartType.SCATTER)) { chart = ChartFactory.createScatterPlot(chartName, xaxisName, yaxisName, xyCollection, PlotOrientation.VERTICAL, showLegend, true, false); } else { throw new IllegalArgumentException("Unknown chart type " + chartType + " for an XY dataset."); } if (chart == null) return null; XYPlot plot = (XYPlot) chart.getPlot(); // XXX the following instance check is brittle; there are many ways to represent a time // series in JFreeChart. This check uses knowledge of the inner workings of DatasetUtil. if (xyCollection instanceof TimePeriodValuesCollection) { logger.debug("Switching x-axis to date axis so labels render properly"); plot.setDomainAxis(new DateAxis(xaxisName)); // TODO user-settable date format // axis.setDateFormatOverride(new SimpleDateFormat("MMM-yyyy")); } if (legendPosition != LegendPosition.NONE) { chart.getLegend().setPosition(legendPosition.getRectangleEdge()); } if (!c.isAutoXAxisRange()) { XYPlot xyplot = chart.getXYPlot(); ValueAxis axis = xyplot.getDomainAxis(); axis.setAutoRange(false); axis.setRange(c.getXAxisMinRange(), c.getXAxisMaxRange()); } if (!c.isAutoYAxisRange()) { XYPlot xyplot = chart.getXYPlot(); ValueAxis axis = xyplot.getRangeAxis(); axis.setAutoRange(false); axis.setRange(c.getYAxisMinRange(), c.getYAxisMaxRange()); } return chart; } }