ca.sqlpower.wabit.swingui.chart.ChartPanel.java Source code

Java tutorial

Introduction

Here is the source code for ca.sqlpower.wabit.swingui.chart.ChartPanel.java

Source

/*
 * 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.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.beans.PropertyChangeEvent;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.swing.AbstractButton;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.JSpinner;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.JToggleButton;
import javax.swing.JToolBar;
import javax.swing.SwingConstants;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;

import net.miginfocom.swing.MigLayout;

import org.apache.log4j.Logger;
import org.jdesktop.swingx.JXLabel;
import org.jfree.chart.JFreeChart;

import ca.sqlpower.object.AbstractPoolingSPListener;
import ca.sqlpower.object.SPListener;
import ca.sqlpower.swingui.table.EditableJTable;
import ca.sqlpower.swingui.table.ResultSetTableModel;
import ca.sqlpower.wabit.WabitObject;
import ca.sqlpower.wabit.WabitUtils;
import ca.sqlpower.wabit.WabitWorkspace;
import ca.sqlpower.wabit.report.chart.Chart;
import ca.sqlpower.wabit.report.chart.ChartDataChangedEvent;
import ca.sqlpower.wabit.report.chart.ChartDataListener;
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.WabitResultSetProducer;
import ca.sqlpower.wabit.swingui.WabitIcons;
import ca.sqlpower.wabit.swingui.WabitPanel;
import ca.sqlpower.wabit.swingui.WabitSwingSession;
import ca.sqlpower.wabit.swingui.WabitToolBarBuilder;
import ca.sqlpower.wabit.swingui.chart.effect.ChartAnimation;

import com.jgoodies.forms.builder.DefaultFormBuilder;
import com.jgoodies.forms.debug.FormDebugPanel;
import com.jgoodies.forms.layout.FormLayout;
import com.itextpdf.text.Font;

/**
 * Provides a complete GUI for setting up and modifying a Wabit Chart object.
 */
public class ChartPanel implements WabitPanel {

    static final Logger logger = Logger.getLogger(ChartPanel.class);

    /**
     * Key for client property stored in the chart chooser toggle buttons.
     */
    private static final String CHART_TYPE_PROP_KEY = ChartPanel.class.getName() + ".CHART_TYPE_PROP_KEY";

    private final Icon BAR_CHART_ICON = new ImageIcon(ChartPanel.class.getResource("/icons/32x32/chart-bar.png"));
    private final Icon LINE_CHART_ICON = new ImageIcon(ChartPanel.class.getResource("/icons/32x32/chart-line.png"));
    private final Icon PIE_CHART_ICON = new ImageIcon(ChartPanel.class.getResource("/icons/32x32/chart-pie.png"));
    private final Icon SCATTER_CHART_ICON = new ImageIcon(
            ChartPanel.class.getResource("/icons/32x32/chart-scatter.png"));

    /**
     * The panel that holds the chart editor UI.
     */
    private final JPanel panel = new JPanel();

    /**
     * Contains all the toggle buttons that are for choosing the chart type.
     * Each button in this group has a client property indicating the {@link ChartType} constant it represents.
     * 
     */
    ButtonGroup chartTypeButtonGroup = new ButtonGroup();

    private final WabitToolBarBuilder toolBarBuilder = new WabitToolBarBuilder();

    /**
     * Contains all the queries in the workspace. This will let the user
     * choose which query to chart.
     */
    private final JComboBox queryComboBox;

    /**
     * Holds the chart's Y axis label.
     */
    private final JTextField yaxisNameField = new JTextField();

    /**
     * A label for the y axis field. This is a member variable so we can make
     * the label invisible when the field is not needed.
     */
    private final JLabel yaxisNameLabel = new JLabel("Y Axis Label");

    /**
     * Holds the chart's X axis label.
     */
    private final JTextField xaxisNameField = new JTextField();

    /**
     * A label for the x axis field. This is a member variable so we can make
     * the label invisible when the field is not needed.
     */
    private final JLabel xaxisNameLabel = new JLabel("X Axis Label");

    /**
     * Slider for the rotation of the x-axis tick labels.
     */
    private final JSlider xaxisLabelRotationSlider = new JSlider(-90, 90);

    /**
     * A label for the x axis rotation slider. This is a member variable so we can make
     * the label invisible when the field is not needed.
     */
    private final JLabel xaxisLabelRotationLabel = new JLabel("Label Rotation");

    /**
     * Checkbox to control the {@link Chart#isGratuitouslyAnimated()} property.
     */
    private JCheckBox gratuitousAnimationCheckbox = new JCheckBox("");

    /**
     * The table that shows values returned from the queries. The headers
     * added to this table will allow users to define which column is the
     * category and which ones are series.
     */
    private final JTable resultTable = new EditableJTable();

    /**
     * Holds the chart's legend position.
     */
    private final JComboBox legendPositionComboBox;

    /**
     * This panel will display a JFreeChart that is a preview of what the
     * user has selected from the result table. This chart should look
     * identical to what would be shown on a report.
     */
    private final org.jfree.chart.ChartPanel chartPanel = new WabitJFreeChartPanel(null);

    /**
     * The label that holds any errors encountered while attempting to create a
     * chart from the query. It should disappear when the chart has been created
     * successfully, but reappear if there's an error.
     * <p>
     * To modify this value (that is, set or clear it), use the {@link #showError()} method.
     */
    private final JXLabel chartError = new JXLabel();

    /**
     * This is the default renderer for the headers of the table displaying the
     * values from the query. It's wrapped by {@link #currentHeaderCellRenderer},
     * which uses it to render the standard headers below our custom
     * "column role" components.
     */
    private final TableCellRenderer defaultHeaderCellRenderer;

    /**
     * Wrapper around the regular {@link #defaultHeaderCellRenderer}. Places the
     * appropriate combo boxes above the standard table headers to allow users
     * to specify the roles of columns (series, categories, or x-axis values) in
     * a chart.
     */
    private ChartTableHeaderCellRenderer currentHeaderCellRenderer;

    /**
     * The header legend of the current table header renderer gets put in the
     * NORTH segment of this panel.
     */
    private JComponent headerLegendContainer = new JPanel(new BorderLayout());

    /**
     * The data model for this component.
     */
    final Chart chart;

    private final JCheckBox xAxisAuto = new JCheckBox();
    private final JLabel xAxisAutoLabel = new JLabel("X axis auto range");
    private final JCheckBox yAxisAuto = new JCheckBox();
    private final JLabel yAxisAutoLabel = new JLabel("Y axis auto range");

    private final JSpinner xAxisMax = new JSpinner();
    private final JLabel xAxisMaxLabel = new JLabel("      X axis max");
    private final JSpinner yAxisMax = new JSpinner();
    private final JLabel yAxisMaxLabel = new JLabel("      Y axis max");
    private final JSpinner xAxisMin = new JSpinner();
    private final JLabel xAxisMinLabel = new JLabel("      X axis min");
    private final JSpinner yAxisMin = new JSpinner();
    private final JLabel yAxisMinLabel = new JLabel("      Y axis min");

    /**
     * Listens for changes to all text fields on this panel. Updates the chart
     * object and its preview on each document event.
     */
    private DocumentListener documentChangeHandler = new DocumentListener() {
        public void changedUpdate(DocumentEvent e) {
            update();
        }

        public void insertUpdate(DocumentEvent e) {
            update();
        }

        public void removeUpdate(DocumentEvent e) {
            update();
        }

        private void update() {
            try {
                skipTextFieldRefresh = true;
                updateChartFromGUI();
            } finally {
                skipTextFieldRefresh = false;
            }
        }
    };

    /**
     * {@link MouseListener} that listens for mouse clicks to update the chart
     * from GUI.
     */
    private MouseListener genericMouseListener = new MouseAdapter() {
        @Override
        public void mouseClicked(MouseEvent e) {
            updateChartFromGUI();
        }
    };

    private ChangeListener genericChangeHandler = new ChangeListener() {
        public void stateChanged(ChangeEvent e) {
            updateChartFromGUI();
        }
    };

    /**
     * Listens to changes in the chart's properties (this component's data
     * model) and updates the GUI when appropriate. Compare with
     * {@link #chartDataListener}, which handles events strictly dealing with
     * the chart's current data (rather than its configuration).
     */
    private final SPListener chartListener = new AbstractPoolingSPListener() {

        /**
         * Set of properties that are ignored with regards to marking this chart
         * panel as having unsaved changes.
         */
        private final Set<String> ignorableProperties = new HashSet<String>();
        {
            ignorableProperties.add("unfilteredResultSet");
        }

        public void propertyChangeImpl(PropertyChangeEvent evt) {
            logger.debug("Got chart property change: \"" + evt.getPropertyName() + "\" " + evt.getOldValue()
                    + " -> " + evt.getNewValue());

            updateGUIFromChart();
        }
    };

    /**
     * Listens to changes to the chart's dataset and updates the chart preview
     * when appropriate. Compare with {@link #chartListener}, which handles
     * events dealing with the chart's current data (rather than its
     * configuration).
     */
    private final ChartDataListener chartDataListener = new ChartDataListener() {
        public void chartDataChanged(ChartDataChangedEvent evt) {
            if (resultTable.getModel() instanceof ResultSetTableModel) {
                ResultSetTableModel rstm = (ResultSetTableModel) resultTable.getModel();
                logger.debug("Updating table model.");
                rstm.dataChanged();
            }
            updateGUIFromChart();
            ChartPanel.this.chartPanel.repaint();
        }
    };

    /**
     * Protects {@link #updateChartFromGUI()} and {@link #updateGUIFromChart()}
     * against mutual recursion.
     * 
     * @see #skipTextFieldRefresh
     */
    private volatile boolean updating = false;

    /**
     * Protects against trying to update text fields when we're already handling
     * a DocumentChanged event.
     * 
     * @see #updating
     */
    private volatile boolean skipTextFieldRefresh = false;

    private final Action refreshDataAction;
    private final Action revertToDefaultsAction;

    /**
     * Listens to all the combo boxes, and updates the chart object whenever an
     * event comes in.
     */
    private final ItemListener genericItemListener = new ItemListener() {
        public void itemStateChanged(ItemEvent e) {
            updateChartFromGUI();
        }
    };

    /**
     * Listens to anything that can produce an action event (such as a button),
     * and updates the chart object whenever an event comes in.
     */
    private final ActionListener genericActionListener = new ActionListener() {
        public void actionPerformed(ActionEvent e) {
            updateChartFromGUI();
        }
    };

    public ChartPanel(WabitSwingSession session, final Chart chart) {
        WabitWorkspace workspace = session.getWorkspace();
        this.chart = chart;

        refreshDataAction = new RefreshDataAction(chart);
        revertToDefaultsAction = new RevertToDefaultsAction(this);

        resultTable.getTableHeader().setReorderingAllowed(false);
        defaultHeaderCellRenderer = resultTable.getTableHeader().getDefaultRenderer();

        List<WabitObject> queries = new ArrayList<WabitObject>();
        queries.addAll(workspace.getQueries());
        queries.addAll(workspace.getOlapQueries());
        queryComboBox = new JComboBox(queries.toArray());

        legendPositionComboBox = new JComboBox(LegendPosition.values());
        yaxisNameField.getDocument().addDocumentListener(documentChangeHandler);
        xaxisNameField.getDocument().addDocumentListener(documentChangeHandler);

        xaxisLabelRotationSlider.addChangeListener(genericChangeHandler);

        gratuitousAnimationCheckbox.addMouseListener(genericMouseListener);

        xAxisAuto.addMouseListener(genericMouseListener);
        yAxisAuto.addMouseListener(genericMouseListener);

        xAxisMax.addChangeListener(genericChangeHandler);
        yAxisMax.addChangeListener(genericChangeHandler);

        xAxisMin.addChangeListener(genericChangeHandler);
        yAxisMin.addChangeListener(genericChangeHandler);

        queryComboBox.addItemListener(genericItemListener);
        legendPositionComboBox.addItemListener(genericItemListener);

        this.chartError.setAlignmentX(JXLabel.CENTER_ALIGNMENT);
        this.chartError.setAlignmentY(JXLabel.CENTER_ALIGNMENT);
        this.chartError.setLineWrap(true);
        this.chartError.setIcon(WabitIcons.CHART_32);

        buildUI();

        //chartPanel.setChart(null);
        chart.addSPListener(chartListener);
        chart.addChartDataListener(chartDataListener);

        chart.refresh();
        updateGUIFromChart();
    }

    /**
     * Displays an error in the chart preview area. The given message replaces
     * any previously existing message. If query execution or anything else
     * fails, this is the appropriate way to hand it.
     * 
     * @param ex
     *            The exception to show. If null, the error component will
     *            become invisible.
     */
    private void showError(String message) {

        logger.debug("Showing exception message in chart editor:" + message);

        chartError.setText(message);

        chartError.setVisible(true);
        chartPanel.setChart(null);
        chartPanel.setVisible(false);
    }

    private void showChart(JFreeChart chartToDisplay) {
        chartError.setText(null);
        chartError.setVisible(false);
        chartPanel.setChart(chartToDisplay);
        chartPanel.setVisible(true);
    }

    /**
     * When any aspect of the chart changes, this method should be called to set
     * the state of the editor. This will set up the correct result set as well
     * as display the correct editor in a new edit state.
     */
    void updateGUIFromChart() {

        if (updating)
            return;

        try {
            updating = true;

            xaxisLabelRotationSlider.setValue((int) chart.getXAxisLabelRotation());

            xAxisAuto.setSelected(chart.isAutoXAxisRange());
            yAxisAuto.setSelected(chart.isAutoYAxisRange());

            xAxisMax.setEnabled(!chart.isAutoXAxisRange());
            xAxisMin.setEnabled(!chart.isAutoXAxisRange());
            yAxisMax.setEnabled(!chart.isAutoYAxisRange());
            yAxisMin.setEnabled(!chart.isAutoYAxisRange());

            xAxisMax.setValue(chart.getXAxisMaxRange());
            yAxisMax.setValue(chart.getYAxisMaxRange());
            xAxisMin.setValue(chart.getXAxisMinRange());
            yAxisMin.setValue(chart.getYAxisMinRange());

            gratuitousAnimationCheckbox.setSelected(chart.isGratuitouslyAnimated());

            queryComboBox.setSelectedItem(chart.getQuery());
            setSelectedChartType(chart.getType());

            if (!skipTextFieldRefresh) {
                yaxisNameField.setText(chart.getYaxisName());
                xaxisNameField.setText(chart.getXaxisName());
            }

            if (chart.getQuery() != null && chart.getUnfilteredResultSet() != null) {

                if (currentHeaderCellRenderer != null) {
                    currentHeaderCellRenderer.cleanup();
                }

                if (chart.getType().getDatasetType() == DatasetType.CATEGORY) {
                    currentHeaderCellRenderer = new CategoryChartHeaderRenderer(this, resultTable.getTableHeader(),
                            defaultHeaderCellRenderer);
                    resultTable.getTableHeader().setDefaultRenderer(currentHeaderCellRenderer);

                    //Set control components visibility based on chart type
                    if (chart.getType() == ChartType.PIE) {
                        xaxisNameLabel.setVisible(false);
                        xaxisNameField.setVisible(false);
                        yaxisNameLabel.setVisible(false);
                        yaxisNameField.setVisible(false);
                        xaxisLabelRotationLabel.setVisible(false);
                        xaxisLabelRotationSlider.setVisible(false);
                        xAxisAuto.setVisible(false);
                        xAxisAutoLabel.setVisible(false);
                        xAxisMax.setVisible(false);
                        xAxisMaxLabel.setVisible(false);
                        xAxisMin.setVisible(false);
                        xAxisMinLabel.setVisible(false);
                        yAxisAuto.setVisible(false);
                        yAxisAutoLabel.setVisible(false);
                        yAxisMax.setVisible(false);
                        yAxisMaxLabel.setVisible(false);
                        yAxisMin.setVisible(false);
                        yAxisMinLabel.setVisible(false);
                    } else {
                        xaxisNameLabel.setVisible(true);
                        xaxisNameField.setVisible(true);
                        yaxisNameLabel.setVisible(true);
                        yaxisNameField.setVisible(true);
                        xaxisLabelRotationLabel.setVisible(true);
                        xaxisLabelRotationSlider.setVisible(true);
                        yAxisAuto.setVisible(true);
                        yAxisAutoLabel.setVisible(true);
                        yAxisMax.setVisible(true);
                        yAxisMaxLabel.setVisible(true);
                        yAxisMin.setVisible(true);
                        yAxisMinLabel.setVisible(true);
                        xAxisAuto.setVisible(false);
                        xAxisAutoLabel.setVisible(false);
                        xAxisMax.setVisible(false);
                        xAxisMaxLabel.setVisible(false);
                        xAxisMin.setVisible(false);
                        xAxisMinLabel.setVisible(false);
                    }

                } else if (chart.getType().getDatasetType() == DatasetType.XY) {
                    try {
                        currentHeaderCellRenderer = new XYChartHeaderRenderer(this, resultTable.getTableHeader(),
                                defaultHeaderCellRenderer);
                    } catch (SQLException e) {
                        showError(WabitUtils.getRootCause(e).getMessage());
                        return;
                    }
                    resultTable.getTableHeader().setDefaultRenderer(currentHeaderCellRenderer);

                    xaxisNameLabel.setVisible(true);
                    xaxisNameField.setVisible(true);
                    yaxisNameLabel.setVisible(true);
                    yaxisNameField.setVisible(true);
                    xaxisLabelRotationLabel.setVisible(false);
                    xaxisLabelRotationSlider.setVisible(false);
                    xAxisAuto.setVisible(true);
                    xAxisAutoLabel.setVisible(true);
                    xAxisMax.setVisible(true);
                    xAxisMaxLabel.setVisible(true);
                    xAxisMin.setVisible(true);
                    xAxisMinLabel.setVisible(true);
                }

                headerLegendContainer.removeAll();
                headerLegendContainer.add(currentHeaderCellRenderer.getHeaderLegendComponent(), BorderLayout.NORTH);
                headerLegendContainer.revalidate();
                headerLegendContainer.repaint();

                if (chart.getLegendPosition() != null) {
                    legendPositionComboBox.setSelectedItem(chart.getLegendPosition());
                } else {
                    legendPositionComboBox.setSelectedItem(LegendPosition.BOTTOM);
                }

                final ResultSetTableModel model = new ResultSetTableModel(chart.getUnfilteredResultSet());
                resultTable.setModel(model);
                ChartTableCellRenderer cellRenderer = new ChartTableCellRenderer(chart);
                for (Enumeration<TableColumn> tableCols = resultTable.getColumnModel().getColumns(); tableCols
                        .hasMoreElements();) {
                    TableColumn tc = tableCols.nextElement();
                    tc.setCellRenderer(cellRenderer);
                }

            } else {
                if (currentHeaderCellRenderer != null) {
                    currentHeaderCellRenderer.cleanup();
                }
                resultTable.getTableHeader().setDefaultRenderer(defaultHeaderCellRenderer);
            }

            updateChartPreview();

        } finally {
            updating = false;
        }
    }

    /**
     * Updates the the JFreeChart preview panel and its associated error
     * message based on the current state of the GUI components in this property
     * panel. The chart generated here is only intended as a preview for the
     * property panel.
     */
    private void updateChartPreview() {
        try {

            if (chart.getQuery() == null) {
                showError("Please select a query from the data source selector above.");
                return;
            }

            if (chart.getUnfilteredResultSet() == null) {
                showError("The selected query does not return any data.");
                return;
            }

            if (chart.findRoleColumns(ColumnRole.CATEGORY).isEmpty() && (chart.getType() == ChartType.BAR
                    || chart.getType() == ChartType.PIE || chart.getType() == ChartType.CATEGORY_LINE)) {
                showError("Map a column to the category role using the controls above.");
                return;
            }
            if (chart.findRoleColumns(ColumnRole.SERIES).isEmpty()) {
                showError("Map a column to the series role using the controls above.");
                return;
            }

            JFreeChart newJFreeChart = ChartSwingUtil.createChartFromQuery(chart);
            logger.debug("Created new JFree chart: " + newJFreeChart);
            this.showChart(newJFreeChart);
            if (chart.isGratuitouslyAnimated()) {
                ChartAnimation.animateIfPossible(newJFreeChart);
            }
        } catch (Exception ex) {
            showError("Unable to create a chart based on the current settings: " + WabitUtils.getRootCause(ex));
        }
    }

    private void buildUI() {

        // First level in the panel has only 2 rows of 1 column
        panel.setLayout(new MigLayout("fill", "[grow, fill]", "[shrink]10[grow, fill]"));

        panel.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(Color.GRAY),
                BorderFactory.createEmptyBorder(5, 5, 5, 5)));

        // Now we build the upper part. 
        // The "data" section.
        JPanel topPanel = new JPanel(new MigLayout("fill", "[115]10[fill, grow]", "[]10[]10[400]"));

        JLabel dataCategoryLabel = new JLabel("Data");
        dataCategoryLabel.setFont(dataCategoryLabel.getFont().deriveFont(Font.BOLD));
        topPanel.add(dataCategoryLabel, "span, wrap");

        topPanel.add(new JLabel("Data source"), "gapleft 15");
        topPanel.add(queryComboBox, "wrap");

        JScrollPane tableScrollPane = new JScrollPane(resultTable);
        topPanel.add(headerLegendContainer, "gapleft 15, aligny top");
        topPanel.add(tableScrollPane);

        panel.add(topPanel, "wrap");

        // Now the lower part. It has two parts. The options scrolling pane and the preview.
        JPanel bottomPanel = new JPanel(
                new MigLayout("fill, hidemode 2", "[grow]10[fill, shrinkprio 101]", "[fill]"));

        JLabel optionsCategoryLabel = new JLabel("Options");
        optionsCategoryLabel.setFont(optionsCategoryLabel.getFont().deriveFont(Font.BOLD));
        bottomPanel.add(optionsCategoryLabel);

        JLabel previewCategoryLabel = new JLabel("Preview");
        previewCategoryLabel.setFont(previewCategoryLabel.getFont().deriveFont(Font.BOLD));
        bottomPanel.add(previewCategoryLabel, "wrap");

        JScrollPane optionsScrollPane = new JScrollPane(buildChartPrefsPanel(),
                JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        optionsScrollPane.setBorder(null);
        bottomPanel.add(optionsScrollPane, "push, gapleft 15");

        JPanel chartAndErrorPanel = new JPanel(new BorderLayout());
        chartAndErrorPanel.add(chartError, BorderLayout.NORTH);
        chartAndErrorPanel.add(chartPanel, BorderLayout.CENTER);
        bottomPanel.add(chartAndErrorPanel, "width 100%, alignx left, gapleft 15");

        panel.add(bottomPanel, "height 100%");

        toolBarBuilder.add(refreshDataAction);
        toolBarBuilder.add(revertToDefaultsAction);

        //Since the first button on the tool bar will be displayed this size will be the
        //same as the font size of a displayed button. If the button wasn't being displayed
        //the font size ends up incorrect
        float fontSize = toolBarBuilder.getToolbar().getComponentAtIndex(0).getFont().getSize();

        toolBarBuilder.addSeparator();
        toolBarBuilder.add(makeChartTypeButton("Bar", ChartType.BAR, BAR_CHART_ICON, fontSize));
        toolBarBuilder.add(makeChartTypeButton("Pie", ChartType.PIE, PIE_CHART_ICON, fontSize));
        toolBarBuilder
                .add(makeChartTypeButton("Category Line", ChartType.CATEGORY_LINE, LINE_CHART_ICON, fontSize));

        toolBarBuilder.addSeparator();
        toolBarBuilder.add(makeChartTypeButton("Line", ChartType.LINE, LINE_CHART_ICON, fontSize));
        toolBarBuilder.add(makeChartTypeButton("Scatter", ChartType.SCATTER, SCATTER_CHART_ICON, fontSize));
    }

    /**
     * Subroutine of {@link #buildUI()}. Makes a chart type toggle button and
     * adds it to the button group.
     * 
     * @param caption
     *            The text to appear under the button
     * @param type
     *            The type of chart the buttons should select
     * @param icon
     *            The icon for the button
     * @param fontSize
     *            the font size for the toggle buttons. The default font size of
     *            the toggle buttons are different than the default font size of
     *            JButtons on some platforms. This value should be equal to the
     *            JButton font size. This is a float as deriving fonts with a size
     *            takes a float.
     * @return A button properly configured for the new-look Wabit toolbar.
     */
    private JToggleButton makeChartTypeButton(String caption, ChartType type, Icon icon, float fontSize) {
        JToggleButton b = new JToggleButton(caption, icon);
        b.putClientProperty(CHART_TYPE_PROP_KEY, type);
        chartTypeButtonGroup.add(b);

        b.setVerticalTextPosition(SwingConstants.BOTTOM);
        b.setHorizontalTextPosition(SwingConstants.CENTER);

        // Removes button borders on OS X 10.5
        b.putClientProperty("JButton.buttonType", "toolbar");

        b.addActionListener(genericActionListener);

        b.setFont(b.getFont().deriveFont(fontSize));

        return b;
    }

    private ChartType getSelectedChartType() {
        for (Enumeration<AbstractButton> buttons = chartTypeButtonGroup.getElements(); buttons.hasMoreElements();) {
            AbstractButton b = buttons.nextElement();
            if (chartTypeButtonGroup.isSelected(b.getModel())) {
                ChartType ct = (ChartType) b.getClientProperty(CHART_TYPE_PROP_KEY);
                logger.debug("Found selected chart type " + ct);
                return ct;
            }
        }

        logger.debug("Didn't find any selected chart buttons. Returning null.");
        return null;
    }

    /**
     * Selects the appropriate button in {@link #chartTypeButtonGroup}.
     * 
     * @param type The type of chart to select the button of.
     */
    private void setSelectedChartType(ChartType type) {
        for (Enumeration<AbstractButton> buttons = chartTypeButtonGroup.getElements(); buttons.hasMoreElements();) {
            AbstractButton b = buttons.nextElement();
            ChartType ct = (ChartType) b.getClientProperty(CHART_TYPE_PROP_KEY);
            if (ct == type) {
                chartTypeButtonGroup.setSelected(b.getModel(), true);
                return;
            }
        }
        throw new IllegalArgumentException("I can't find a button for chart type " + type);
    }

    /**
     * Subroutine of {@link #buildUI()}. Creates the form that appears to the
     * right of the JFreeChart preview.
     */
    private Component buildChartPrefsPanel() {
        DefaultFormBuilder builder = new DefaultFormBuilder(new FormLayout("70dlu, 3dlu, 90dlu"),
                logger.isDebugEnabled() ? new FormDebugPanel() : new JPanel());

        builder.append("Legend Postion", legendPositionComboBox);
        builder.nextLine();

        builder.append(yaxisNameLabel, yaxisNameField);
        builder.nextLine();

        builder.append(xaxisNameLabel, xaxisNameField);
        builder.nextLine();

        builder.append(xaxisLabelRotationLabel, xaxisLabelRotationSlider);
        builder.nextLine();

        builder.append(this.xAxisAutoLabel, this.xAxisAuto);
        builder.append(this.xAxisMaxLabel, this.xAxisMax);
        builder.append(this.xAxisMinLabel, this.xAxisMin);
        builder.nextLine();

        builder.append(this.yAxisAutoLabel, this.yAxisAuto);
        builder.append(this.yAxisMaxLabel, this.yAxisMax);
        builder.append(this.yAxisMinLabel, this.yAxisMin);
        builder.nextLine();

        builder.append("Gratuitous Animation", gratuitousAnimationCheckbox);

        return builder.getPanel();
    }

    public boolean hasUnsavedChanges() {
        //TODO replace the false with the chartHasChanges flag
        //when fixing bug 2002.
        return false;
    }

    public JComponent getPanel() {
        return panel;
    }

    public void discardChanges() {
        logger.debug("Discarding changes");
        cleanup();
        updateChartFromGUI(); //TODO remove this when fixing bug 2002.
    }

    public boolean applyChanges() {
        logger.debug("Applying changes");
        cleanup();
        updateChartFromGUI();
        return true;
    }

    private void cleanup() {
        chart.removeSPListener(chartListener);
        chart.removeChartDataListener(chartDataListener);
    }

    /**
     * Updates {@link #chart}'s settings to match those currently in this
     * panel's GUI components. This is the inverse of {@link #updateGUIFromChart()}.
     */
    void updateChartFromGUI() {
        if (updating)
            return;
        try {
            updating = true;

            if (getSelectedChartType() != chart.getType()) {
                chart.setType(getSelectedChartType());
            }

            chart.setLegendPosition((LegendPosition) legendPositionComboBox.getSelectedItem());
            chart.setYaxisName(yaxisNameField.getText());
            chart.setXaxisName(xaxisNameField.getText());
            chart.setXAxisLabelRotation(xaxisLabelRotationSlider.getValue());
            chart.setGratuitouslyAnimated(gratuitousAnimationCheckbox.isSelected());
            chart.setAutoXAxisRange(xAxisAuto.isSelected());
            chart.setAutoYAxisRange(yAxisAuto.isSelected());
            chart.setXAxisMaxRange(Double.parseDouble(xAxisMax.getValue().toString()));
            chart.setYAxisMaxRange(Double.parseDouble(yAxisMax.getValue().toString()));
            chart.setXAxisMinRange(Double.parseDouble(xAxisMin.getValue().toString()));
            chart.setYAxisMinRange(Double.parseDouble(yAxisMin.getValue().toString()));

            if (queryComboBox.getSelectedItem() != chart.getQuery()) {
                chart.setQuery((WabitResultSetProducer) queryComboBox.getSelectedItem());
                chart.refresh();
            }

        } finally {
            updating = false;
        }

        /*
         * Because we were purposely ignoring events from the chart while updating it,
         * we now have to do an explicit refresh of the table and chart preview. The
         * same interlock that prevented mutual recursion during the GUI->Chart update
         * prevents Chart->GUI updates from re-calling this method.
         */
        updateGUIFromChart();
    }

    public String getTitle() {
        return "Chart Editor - " + chart.getName();
    }

    public JComponent getSourceComponent() {
        // nothing drags onto this
        return null;
    }

    public Chart getChart() {
        return chart;
    }

    /**
     * Returns the leading X coordinate (within the table of data) of the given
     * column. This is a helper method for the table header cell renderers.
     */
    int getXPositionOfColumn(final TableColumnModel model, final int columnIndex) {
        int sum = 0;
        for (int i = 0; i < columnIndex; i++) {
            sum += model.getColumn(i).getWidth();
        }
        logger.debug("X position of column " + columnIndex + " is " + sum + " according to " + model);
        return sum;
    }

    public JToolBar getToolbar() {
        return toolBarBuilder.getToolbar();
    }

}