package com.ibm.nmon.gui.chart; import org.slf4j.Logger; import java.awt.Graphics; import java.awt.Point; import java.awt.Toolkit; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.StringSelection; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.io.IOException; import java.io.StringWriter; import java.util.List; import javax.swing.JFrame; import javax.swing.JFileChooser; import javax.swing.JOptionPane; import javax.swing.JPopupMenu; import javax.swing.MenuElement; import javax.swing.JMenuItem; import org.jfree.chart.ChartPanel; import org.jfree.chart.ChartTransferable; import org.jfree.chart.ChartUtilities; import org.jfree.chart.JFreeChart; import org.jfree.chart.annotations.Annotation; import org.jfree.chart.plot.Plot; import org.jfree.chart.plot.CategoryPlot; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.plot.Marker; import org.jfree.ui.ExtensionFileFilter; import com.ibm.nmon.gui.chart.data.*; import com.ibm.nmon.gui.main.NMONVisualizerGui; import com.ibm.nmon.gui.file.GUIFileChooser; import com.ibm.nmon.util.CSVWriter; public class BaseChartPanel extends ChartPanel implements PropertyChangeListener { private static final long serialVersionUID = -1342720624336005568L; protected final Logger logger = org.slf4j.LoggerFactory.getLogger(getClass()); protected final NMONVisualizerGui gui; protected final JFrame parent; // used by subclasses for adding annotations protected java.awt.Point clickLocation = null; private int saveHeight; private int saveWidth; protected BaseChartPanel(NMONVisualizerGui gui, JFrame parent) { // With multiple charts for each ReportPanel and one report panel per DataSet, there could // be a large number of active charts. To keep memory usage down, disable using a buffered // image despite any performance benefit. // Unfortunately, chartBuffer in ChartPanel is private so there is no way to clear the // memory used when the chart is not being displayed. super(null, DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_MINIMUM_DRAW_WIDTH, DEFAULT_MINIMUM_DRAW_HEIGHT, DEFAULT_MAXIMUM_DRAW_WIDTH, DEFAULT_MAXIMUM_DRAW_HEIGHT, false, true, false, false, false, true); setBackground(java.awt.Color.WHITE); // keep the chart for resizing when empty setPreferredSize(null); // disable zooming removeMouseMotionListener(this); // no need for mouse listener when there is no chart removeMouseListener(this); this.gui = gui; this.parent = parent; this.saveWidth = 1920 / 2; this.saveHeight = 1080 / 2; setEnabled(false); clearChart(); setEnforceFileExtensions(true); } public final void setChart(JFreeChart chart) { JFreeChart old = getChart(); if (chart != old) { super.setChart(chart); if (chart != null) { // keep in sync with clearChart(); only add a single mouse listener if (old == null) { addMouseListener(this); } firePropertyChange("chart", null, this); } else { // fix memory leak; rendering info holds ChartEntitiy objects that reference the // chart's Dataset getChartRenderingInfo().clear(); firePropertyChange("chart", null, null); } } } public void clearChart() { if (getChart() != null) { setChart(null); // do not display the popup menu when there is no chart removeMouseListener(this); } } /** * <p> * Visually indicate on the that the element (bar or line) corresponding to the <code>Dataset's</code> given row and * column. * </p> * * <p> * This method <em>should not</em> fire a property change event since this is meant to be called in response to * other user input. This function should toggle already highlighted elements to an unhighlighted state. * </p> * * @param row the chart's dataset row * @param column the chart's dataset column */ public void highlightElement(int row, int column) {} /** * Remove all highlights from chart. */ public void clearHighlightedElements() {} public void setElementVisible(int row, int column, boolean visible) {}; /** * Get the dataset associated with this chart or <code>null</code> if there the chart does not have one. */ public final DataTupleDataset getDataset() { if (getChart() == null) { return null; } Plot plot = getChart().getPlot(); if (plot instanceof XYPlot) { XYPlot xyPlot = getChart().getXYPlot(); DataTupleDataset dataset = (DataTupleDataset) xyPlot.getDataset(0); if (xyPlot.getDatasetCount() == 1) { return dataset; } else { // assume only 2 datasets / 2 axes if (dataset instanceof DataTupleXYDataset) { return ((DataTupleXYDataset) dataset).merge((DataTupleXYDataset) xyPlot.getDataset(1)); } else if (dataset instanceof DataTupleHistogramDataset) { return ((DataTupleHistogramDataset) dataset) .merge((DataTupleHistogramDataset) xyPlot.getDataset(1)); } else { logger.warn("unknown DataTupleDataset class {}, returning null", dataset.getClass().getName()); return null; } } } else if (plot instanceof CategoryPlot) { CategoryPlot categoryPlot = (CategoryPlot) plot; DataTupleCategoryDataset dataset = (DataTupleCategoryDataset) categoryPlot.getDataset(0); if (categoryPlot.getDatasetCount() == 1) { return dataset; } else { return dataset.merge((DataTupleCategoryDataset) categoryPlot.getDataset(1)); } } else { return null; } } // update the base class methods to put out images of a fixed size rather than the current size // of the chart in the gui @Override public final void doCopy() { Clipboard systemClipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); ChartTransferable selection = new ChartTransferable(getChart(), saveWidth, saveHeight); systemClipboard.setContents(selection, null); } public final void doCopyDataset() { DataTupleDataset data = getDataset(); if (data != null) { StringWriter writer = new StringWriter(2048); try { CSVWriter.write(data, writer); Clipboard systemClipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); systemClipboard.setContents(new StringSelection(writer.toString()), null); } catch (IOException ioe) { logger.error("error copying dataset", ioe); } } } @Override public final void doSaveAs() throws IOException { String directory = gui.getPreferences().get("lastSaveDirectory", "./"); String filename = validateSaveFileName(null); File chartFile = new File(directory, filename); GUIFileChooser fileChooser = new GUIFileChooser(gui, "Select Save Location"); fileChooser.setSelectedFile(chartFile); ExtensionFileFilter filter = new ExtensionFileFilter(localizationResources.getString("PNG_Image_Files"), ".png"); fileChooser.addChoosableFileFilter(filter); if (fileChooser.showDialog(this, "Save") == JFileChooser.APPROVE_OPTION) { chartFile = fileChooser.getSelectedFile(); if (isEnforceFileExtensions()) { if (!chartFile.getName().endsWith(".png")) { chartFile = new File(chartFile.getAbsolutePath() + ".png"); } } if (chartFile.exists()) { int result = JOptionPane.showConfirmDialog(gui.getMainFrame(), "File '" + chartFile.getName() + "' already exists.\nDo you want to overwrite it?", "Overwrite?", JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE); if (result != JOptionPane.OK_OPTION) { return; } } ChartUtilities.saveChartAsPNG(chartFile, getChart(), saveWidth, saveHeight); } // save the last directory whether or not the file is actually saved gui.getPreferences().put("lastSaveDirectory", chartFile.getParentFile().getAbsolutePath()); } public final void setSaveSize(int width, int height) { if (width < 1) { throw new IllegalArgumentException("width" + "must be greater than 0"); } if (height < 1) { throw new IllegalArgumentException("height" + "must be greater than 0"); } saveWidth = width; saveHeight = height; } public final void saveChart(String directory, String filename) { filename = validateSaveFileName(filename); File chartFile = new File(directory, filename); try { ChartUtilities.saveChartAsPNG(chartFile, getChart(), saveWidth, saveHeight); } catch (IOException ioe) { logger.error("could not save chart '" + filename + "' to directory '" + directory + "'", ioe); } } public void addAnnotations(List<Annotation> annotations) {} public void addMarkers(List<Marker> markers) {} protected String validateSaveFileName(String filename) { if ((filename == null) || "".equals(filename)) { String title = getChart().getTitle().getText(); if ((title == null) || "".equals(title)) { filename = "chart_" + this.hashCode(); } else { filename = title; } } if (isEnforceFileExtensions() && !filename.endsWith(".png")) { filename += ".png"; } filename = filename.replace('\n', ' '); filename = filename.replace(' ', '_'); return filename; } @Override protected JPopupMenu createPopupMenu(boolean properties, boolean copy, boolean save, boolean print, boolean zoom) { JPopupMenu popup = super.createPopupMenu(properties, copy, save, print, zoom); int n = 0; // find the existing 'Copy' menu item and add an option to copy chart data after that for (MenuElement element : popup.getSubElements()) { JMenuItem item = (JMenuItem) element; if (item.getText().equals("Copy")) { JMenuItem copyData = new JMenuItem("Copy Chart Data"); copyData.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { doCopyDataset(); } }); // after separator, after copy => + 2 popup.add(copyData, n + 2); popup.add(new JPopupMenu.Separator(), n + 3); } n++; } // create Save Chart item // note that the default 'Save as' item is no present since false was passed into the // BaseChartPanel constructor when creating this class' instance JMenuItem savePNG = new JMenuItem("Save Chart..."); savePNG.setActionCommand("SAVE_AS_PNG"); savePNG.addActionListener(this); popup.add(savePNG); return popup; } @Override public void propertyChange(PropertyChangeEvent evt) {} // disable mouse movement to avoid constantly searching for ChartEntity objects when there are // ChartMouseListeners // assume subclasses will not need the functionality (zooming / panning) provided in the super // class @Override public void mouseMoved(MouseEvent e) {} @Override public void mouseDragged(MouseEvent e) {} // track location of the mouse event for annotations @Override public void mousePressed(MouseEvent event) { if (event.isPopupTrigger()) { clickLocation = new Point(event.getX(), event.getY()); } super.mousePressed(event); } @Override public void mouseReleased(MouseEvent event) { if (event.isPopupTrigger()) { clickLocation = new Point(event.getX(), event.getY()); } super.mouseReleased(event); } @Override public void paintComponent(Graphics g) { if (logger.isTraceEnabled()) { long start = System.nanoTime(); super.paintComponent(g); String title = "<no title>"; if ((getChart() != null) && (getChart().getTitle()) != null) { title = getChart().getTitle().getText(); } logger.trace("painted chart '{}' in {} ms", title, (System.nanoTime() - start) / 1000000.0d); } else { super.paintComponent(g); } } }