/*
* This program 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 2 of the License, or
* (at your option) any later version.
*
* This program 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, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
/*
* StripChart.java
* Copyright (C) 2002 University of Waikato, Hamilton, New Zealand
*
*/
package weka.gui.beans;
import weka.core.Instance;
import weka.core.Instances;
import weka.gui.visualize.PrintableComponent;
import weka.gui.visualize.VisualizeUtils;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Image;
import java.beans.EventSetDescriptor;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.Random;
import java.util.Vector;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.border.TitledBorder;
/**
* Bean that can display a horizontally scrolling strip chart. Can
* display multiple plots simultaneously
*
* @author Mark Hall
* @version $Revision: 4806 $
*/
public class StripChart
extends JPanel
implements ChartListener, InstanceListener, Visible,
BeanCommon, UserRequestAcceptor {
/** for serialization */
private static final long serialVersionUID = 1483649041577695019L;
/** default colours for colouring lines */
protected Color [] m_colorList = {Color.green,
Color.red,
Color.blue,
Color.cyan,
Color.pink,
new Color(255, 0, 255),
Color.orange,
new Color(255, 0, 0),
new Color(0, 255, 0),
Color.white};
/** the background color. */
protected Color m_BackgroundColor;
/** the color of the legend panel's border. */
protected Color m_LegendPanelBorderColor;
/**
* Class providing a panel for the plot.
*/
private class StripPlotter
extends JPanel {
/** for serialization. */
private static final long serialVersionUID = -7056271598761675879L;
public void paintComponent(Graphics g) {
super.paintComponent(g);
if (m_osi != null) {
g.drawImage(m_osi,0,0,this);
}
}
}
private transient JFrame m_outputFrame = null;
private transient StripPlotter m_plotPanel = null;
/**
* The off screen image for rendering to.
*/
private transient Image m_osi = null;
/**
* Width and height of the off screen image.
*/
private int m_iheight;
private int m_iwidth;
/**
* Max value for the y axis.
*/
private double m_max = 1;
/**
* Min value for the y axis.
*/
private double m_min = 0;
/**
* Scale update requested.
*/
private boolean m_yScaleUpdate = false;
private double m_oldMax;
private double m_oldMin;
private Font m_labelFont = new Font("Monospaced", Font.PLAIN, 10);
private FontMetrics m_labelMetrics;
// private int m_plotCount = 0;
private Vector m_legendText = new Vector();
/**
* Class providing a panel for displaying the y axis.
*/
private class ScalePanel
extends JPanel {
/** for serialization. */
private static final long serialVersionUID = 6416998474984829434L;
public void paintComponent(Graphics gx) {
super.paintComponent(gx);
if (m_labelMetrics == null) {
m_labelMetrics = gx.getFontMetrics(m_labelFont);
}
gx.setFont(m_labelFont);
int hf = m_labelMetrics.getAscent();
String temp = ""+m_max;
gx.setColor(m_colorList[m_colorList.length-1]);
gx.drawString(temp, 1, hf-2);
temp = ""+(m_min + ((m_max - m_min)/2.0));
gx.drawString(temp, 1, (this.getHeight() / 2)+(hf / 2));
temp = ""+m_min;
gx.drawString(temp, 1, this.getHeight()-1);
}
};
/** the scale. */
private ScalePanel m_scalePanel = new ScalePanel();
/**
* Class providing a panel for the legend.
*/
private class LegendPanel
extends JPanel {
/** for serialization. */
private static final long serialVersionUID = 7713986576833797583L;
public void paintComponent(Graphics gx) {
super.paintComponent(gx);
if (m_labelMetrics == null) {
m_labelMetrics = gx.getFontMetrics(m_labelFont);
}
int hf = m_labelMetrics.getAscent();
int x = 10; int y = hf+15;
gx.setFont(m_labelFont);
for (int i = 0; i < m_legendText.size(); i++) {
String temp = (String)m_legendText.elementAt(i);
gx.setColor(m_colorList[(i % m_colorList.length)]);
gx.drawString(temp,x,y);
y+=hf;
}
StripChart.this.revalidate();
}
};
/** the legend. */
private LegendPanel m_legendPanel = new LegendPanel();
/**
* Holds the data to be plotted. Entries in the list are arrays of
* y points.
*/
private LinkedList m_dataList = new LinkedList();
// private double [] m_dataPoint = new double[1];
private double [] m_previousY = new double[1];
private transient Thread m_updateHandler;
protected BeanVisual m_visual =
new BeanVisual("StripChart",
BeanVisual.ICON_PATH+"StripChart.gif",
BeanVisual.ICON_PATH+"StripChart_animated.gif");
private Object m_listenee = null;
private transient weka.gui.Logger m_log = null;
/**
* Print x axis labels every m_xValFreq points
*/
private int m_xValFreq = 500;
private int m_xCount = 0;
/**
* Shift the plot by this many pixels every time a point is plotted
*/
private int m_refreshWidth = 1;
/**
* Plot every m_refreshFrequency'th point
*/
private int m_refreshFrequency = 5;
/** the class responsible for printing */
protected PrintableComponent m_Printer = null;
public StripChart() {
// m_plotPanel.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
setLayout(new BorderLayout());
add(m_visual, BorderLayout.CENTER);
// start a thread to handle plot updates
initPlot();
}
/**
* Set a custom (descriptive) name for this bean
*
* @param name the name to use
*/
public void setCustomName(String name) {
m_visual.setText(name);
}
/**
* Get the custom (descriptive) name for this bean (if one has been set)
*
* @return the custom name (or the default name)
*/
public String getCustomName() {
return m_visual.getText();
}
/**
* Global info for this bean
*
* @return a String
value
*/
public String globalInfo() {
return "Visualize incremental classifier performance as a scrolling plot.";
}
/**
* GUI Tip text
*
* @return a String
value
*/
public String xLabelFreqTipText() {
return "Show x axis labels this often";
}
/**
* Set the frequency for printing x label values
*
* @param freq an int
value
*/
public void setXLabelFreq(int freq) {
m_xValFreq = freq;
if (getGraphics() != null)
setRefreshWidth();
}
/**
* Get the frequency by which x axis values are printed
*
* @return an int
value
*/
public int getXLabelFreq() {
return m_xValFreq;
}
/**
* GUI Tip text
*
* @return a String
value
*/
public String refreshFreqTipText() {
return "Plot every x'th data point";
}
/**
* Set how often (in x axis points) to refresh the display
*
* @param freq an int
value
*/
public void setRefreshFreq(int freq) {
m_refreshFrequency = freq;
if (getGraphics() != null)
setRefreshWidth();
}
/**
* Get the refresh frequency
*
* @return an int
value
*/
public int getRefreshFreq() {
return m_refreshFrequency;
}
private void setRefreshWidth() {
m_refreshWidth = 1;
if (m_labelMetrics == null) {
getGraphics().setFont(m_labelFont);
m_labelMetrics = getGraphics().getFontMetrics(m_labelFont);
}
int refWidth = m_labelMetrics.stringWidth("99000");
// compute how often x label will be rendered
int z = (getXLabelFreq() / getRefreshFreq());
if (z < 1) {
z = 1;
}
if (z * m_refreshWidth < refWidth+5) {
m_refreshWidth *= (((refWidth+5) / z) + 1) ;
}
}
/**
* Provide some necessary initialization after object has
* been deserialized.
*
* @param ois an ObjectInputStream
value
* @exception IOException if an error occurs
* @exception ClassNotFoundException if an error occurs
*/
private void readObject(ObjectInputStream ois)
throws IOException, ClassNotFoundException {
try {
ois.defaultReadObject();
initPlot();
// startHandler();
} catch (Exception ex) {
ex.printStackTrace();
}
}
/**
* Loads properties from properties file.
*
* @see KnowledgeFlowApp#BEAN_PROPERTIES
*/
private void setProperties() {
String key;
String color;
// background color
key = this.getClass().getName() + ".backgroundColour";
color = KnowledgeFlowApp.BEAN_PROPERTIES.getProperty(key);
m_BackgroundColor = Color.BLACK;
if (color != null)
m_BackgroundColor = VisualizeUtils.processColour(color, m_BackgroundColor);
// legend color (border)
key = m_legendPanel.getClass().getName() + ".borderColour";
color = KnowledgeFlowApp.BEAN_PROPERTIES.getProperty(key);
m_LegendPanelBorderColor = Color.BLUE;
if (color != null)
m_LegendPanelBorderColor = VisualizeUtils.processColour(color, m_LegendPanelBorderColor);
}
private void initPlot() {
setProperties();
m_plotPanel = new StripPlotter();
m_plotPanel.setBackground(m_BackgroundColor);
m_scalePanel.setBackground(m_BackgroundColor);
m_legendPanel.setBackground(m_BackgroundColor);
m_xCount = 0;
}
private void startHandler() {
if (m_updateHandler == null) {
m_updateHandler = new Thread() {
private double [] dataPoint;
public void run() {
while (true) {
if (m_outputFrame != null) {
synchronized(m_dataList) {
while(m_dataList.isEmpty()) {
// while (m_dataList.empty()) {
try {
m_dataList.wait();
} catch (InterruptedException ex) {
return;
}
}
dataPoint = (double [])m_dataList.remove(0);
//dataPoint = (double [])m_dataList.pop();
}
if (m_outputFrame != null) {
StripChart.this.updateChart(dataPoint);
}
}
}
}
};
// m_updateHandler.setPriority(Thread.MIN_PRIORITY);
m_updateHandler.start();
}
}
/**
* Popup the chart panel
*/
public void showChart() {
if (m_outputFrame == null) {
m_outputFrame = new JFrame("Strip Chart");
m_outputFrame.getContentPane().setLayout(new BorderLayout());
JPanel panel = new JPanel(new BorderLayout());
new PrintableComponent(panel);
m_outputFrame.getContentPane().add(panel, BorderLayout.CENTER);
panel.add(m_legendPanel, BorderLayout.WEST);
panel.add(m_plotPanel, BorderLayout.CENTER);
panel.add(m_scalePanel, BorderLayout.EAST);
m_legendPanel.setMinimumSize(new Dimension(100,getHeight()));
m_legendPanel.setPreferredSize(new Dimension(100,getHeight()));
m_scalePanel.setMinimumSize(new Dimension(30, getHeight()));
m_scalePanel.setPreferredSize(new Dimension(30, getHeight()));
Font lf = new Font("Monospaced", Font.PLAIN, 12);
m_legendPanel.setBorder(BorderFactory.
createTitledBorder(BorderFactory.
createEtchedBorder(Color.gray,
Color.darkGray),
"Legend" ,
TitledBorder.CENTER,
TitledBorder.DEFAULT_POSITION, lf,
m_LegendPanelBorderColor));
m_outputFrame.addWindowListener(new java.awt.event.WindowAdapter() {
public void windowClosing(java.awt.event.WindowEvent e) {
if (m_updateHandler != null) {
System.err.println("Interrupting");
m_updateHandler.interrupt();
m_updateHandler = null;
}
synchronized (m_dataList) {
m_dataList = new LinkedList();
}
m_outputFrame.dispose();
m_outputFrame = null;
}
});
m_outputFrame.pack();
m_outputFrame.setSize(600,150);
m_outputFrame.setResizable(false);
m_outputFrame.setVisible(true);
int iwidth = m_plotPanel.getWidth();
int iheight = m_plotPanel.getHeight();
m_osi = m_plotPanel.createImage(iwidth, iheight);
Graphics m = m_osi.getGraphics();
m.setColor(m_BackgroundColor);
m.fillRect(0,0,iwidth,iheight);
m_previousY[0] = -1;
setRefreshWidth();
if (m_updateHandler == null) {
System.err.println("Starting handler");
startHandler();
}
} else {
m_outputFrame.toFront();
}
}
private int convertToPanelY(double yval) {
int height = m_plotPanel.getHeight();
double temp = (yval - m_min) / (m_max - m_min);
temp = temp * height;
temp = height - temp;
return (int)temp;
}
/**
* Update the plot
*
* @param dataPoint contains y values to plot
*/
protected void updateChart(double [] dataPoint) {
if (m_previousY[0] == -1) {
int iw = m_plotPanel.getWidth();
int ih = m_plotPanel.getHeight();
m_osi = m_plotPanel.createImage(iw, ih);
Graphics m = m_osi.getGraphics();
m.setColor(m_BackgroundColor);
m.fillRect(0,0,iw,ih);
m_previousY[0] = convertToPanelY(0);
m_iheight = ih; m_iwidth = iw;
}
if (dataPoint.length-1 != m_previousY.length) {
m_previousY = new double [dataPoint.length-1];
// m_plotCount = 0;
for (int i = 0; i < dataPoint.length-1; i++) {
m_previousY[i] = convertToPanelY(0);
}
}
Graphics osg = m_osi.getGraphics();
Graphics g = m_plotPanel.getGraphics();
osg.copyArea(m_refreshWidth,0,m_iwidth-m_refreshWidth,
m_iheight,-m_refreshWidth,0);
osg.setColor(m_BackgroundColor);
osg.fillRect(m_iwidth-m_refreshWidth,0, m_iwidth, m_iheight);
// paint the old scale onto the plot if a scale update has occured
if (m_yScaleUpdate) {
String maxVal = numToString(m_oldMax);
String minVal = numToString(m_oldMin);
String midVal = numToString((m_oldMax - m_oldMin) / 2.0);
if (m_labelMetrics == null) {
m_labelMetrics = g.getFontMetrics(m_labelFont);
}
osg.setFont(m_labelFont);
int wmx = m_labelMetrics.stringWidth(maxVal);
int wmn = m_labelMetrics.stringWidth(minVal);
int wmd = m_labelMetrics.stringWidth(midVal);
int hf = m_labelMetrics.getAscent();
osg.setColor(m_colorList[m_colorList.length-1]);
osg.drawString(maxVal, m_iwidth-wmx, hf-2);
osg.drawString(midVal, m_iwidth-wmd, (m_iheight / 2)+(hf / 2));
osg.drawString(minVal, m_iwidth-wmn, m_iheight-1);
m_yScaleUpdate = false;
}
double pos;
for (int i = 0; i < dataPoint.length-1; i++) {
osg.setColor(m_colorList[(i % m_colorList.length)]);
pos = convertToPanelY(dataPoint[i]);
osg.drawLine(m_iwidth-m_refreshWidth, (int)m_previousY[i],
m_iwidth-1, (int)pos);
m_previousY[i] = pos;
if (dataPoint[dataPoint.length-1] % m_xValFreq == 0) {
// draw the actual y value onto the plot for this curve
String val = numToString(dataPoint[i]);
if (m_labelMetrics == null) {
m_labelMetrics = g.getFontMetrics(m_labelFont);
}
int hf = m_labelMetrics.getAscent();
if (pos - hf < 0) {
pos += hf;
}
int w = m_labelMetrics.stringWidth(val);
osg.setFont(m_labelFont);
osg.drawString(val, m_iwidth-w, (int)pos);
}
}
// last element in the data point array contains the data point number
if (dataPoint[dataPoint.length-1] % m_xValFreq == 0) {
String xVal = ""+(int)dataPoint[dataPoint.length-1];
osg.setColor(m_colorList[m_colorList.length-1]);
int w = m_labelMetrics.stringWidth(xVal);
osg.setFont(m_labelFont);
osg.drawString(xVal, m_iwidth-w, m_iheight - 1);
}
g.drawImage(m_osi,0,0,m_plotPanel);
// System.err.println("Finished");
// m_plotCount++;
}
private static String numToString(double num) {
int precision = 1;
int whole = (int)Math.abs(num);
double decimal = Math.abs(num) - whole;
int nondecimal;
nondecimal = (whole > 0)
? (int)(Math.log(whole) / Math.log(10))
: 1;
precision = (decimal > 0)
? (int)Math.abs(((Math.log(Math.abs(num)) /
Math.log(10))))+2
: 1;
if (precision > 5) {
precision = 1;
}
String numString = weka.core.Utils.doubleToString(num,
nondecimal+1+precision
,precision);
return numString;
}
ChartEvent m_ce = new ChartEvent(this);
double [] m_dataPoint = null;
public void acceptInstance(InstanceEvent e) {
if (e.getStatus() == InstanceEvent.FORMAT_AVAILABLE) {
Instances structure = e.getStructure();
m_legendText = new Vector();
m_max = 1.0;
m_min = 0;
int i = 0;
for (i =0; i < structure.numAttributes(); i++) {
if (i > 10) {
i--;
break;
}
m_legendText.addElement(structure.attribute(i).name());
m_legendPanel.repaint();
m_scalePanel.repaint();
}
m_dataPoint = new double[i];
m_xCount = 0;
return;
}
// process data point
Instance inst = e.getInstance();
for (int i = 0; i < m_dataPoint.length; i++) {
if (!inst.isMissing(i)) {
m_dataPoint[i] = inst.value(i);
}
}
acceptDataPoint(m_dataPoint);
m_xCount++;
}
/**
* Accept a data point (encapsulated in a chart event) to plot
*
* @param e a ChartEvent
value
*/
public void acceptDataPoint(ChartEvent e) {
if (e.getReset()) {
m_xCount = 0;
m_max = 1;
m_min = 0;
}
if (m_outputFrame != null) {
boolean refresh = false;
if (e.getLegendText() != null & e.getLegendText() != m_legendText) {
m_legendText = e.getLegendText();
refresh = true;
}
if (e.getMin() != m_min || e.getMax() != m_max) {
m_oldMax = m_max; m_oldMin = m_min;
m_max = e.getMax();
m_min = e.getMin();
refresh = true;
m_yScaleUpdate = true;
}
if (refresh) {
m_legendPanel.repaint();
m_scalePanel.repaint();
}
acceptDataPoint(e.getDataPoint());
}
m_xCount++;
}
/**
* Accept a data point to plot
*
* @param dataPoint a double[]
value
*/
public void acceptDataPoint(double [] dataPoint) {
if (m_outputFrame != null && (m_xCount % m_refreshFrequency == 0 )) {
double [] dp = new double[dataPoint.length+1];
dp[dp.length-1] = m_xCount;
System.arraycopy(dataPoint, 0, dp, 0, dataPoint.length);
// check for out of scale values
for (int i = 0; i < dataPoint.length; i++) {
if (dataPoint[i] < m_min) {
m_oldMin = m_min; m_min = dataPoint[i];
m_yScaleUpdate = true;
}
if (dataPoint[i] > m_max) {
m_oldMax = m_max; m_max = dataPoint[i];
m_yScaleUpdate = true;
}
}
if (m_yScaleUpdate) {
m_scalePanel.repaint();
m_yScaleUpdate = false;
}
synchronized(m_dataList) {
m_dataList.add(m_dataList.size(), dp);
// m_dataList.push(dp);
m_dataList.notifyAll();
/* if (m_dataList.size() != 0) {
System.err.println("***** "+m_dataList.size());
} */
// System.err.println(m_xCount);
}
}
}
/**
* Set the visual appearance of this bean
*
* @param newVisual a BeanVisual
value
*/
public void setVisual(BeanVisual newVisual) {
m_visual = newVisual;
}
/**
* Get the visual appearance of this bean
*/
public BeanVisual getVisual() {
return m_visual;
}
/**
* Use the default visual appearance for this bean
*/
public void useDefaultVisual() {
m_visual.loadIcons(BeanVisual.ICON_PATH+"StripChart.gif",
BeanVisual.ICON_PATH+"StripChart_animated.gif");
}
/**
* Stop any processing that the bean might be doing.
*/
public void stop() {
// tell the listenee (upstream bean) to stop
if (m_listenee instanceof BeanCommon) {
((BeanCommon)m_listenee).stop();
}
}
/**
* Returns true if. at this time, the bean is busy with some
* (i.e. perhaps a worker thread is performing some calculation).
*
* @return true if the bean is busy.
*/
public boolean isBusy() {
return (m_updateHandler != null);
}
/**
* Set a logger
*
* @param logger a weka.gui.Logger
value
*/
public void setLog(weka.gui.Logger logger) {
m_log = logger;
}
/**
* Returns true if, at this time,
* the object will accept a connection via the named event
*
* @param eventName the name of the event
* @return true if the object will accept a connection
*/
public boolean connectionAllowed(String eventName) {
if (m_listenee == null) {
return true;
}
return false;
}
/**
* Returns true if, at this time,
* the object will accept a connection according to the supplied
* EventSetDescriptor
*
* @param esd the EventSetDescriptor
* @return true if the object will accept a connection
*/
public boolean connectionAllowed(EventSetDescriptor esd) {
return connectionAllowed(esd.getName());
}
/**
* Notify this object that it has been registered as a listener with
* a source for recieving events described by the named event
* This object is responsible for recording this fact.
*
* @param eventName the event
* @param source the source with which this object has been registered as
* a listener
*/
public void connectionNotification(String eventName, Object source) {
if (connectionAllowed(eventName)) {
m_listenee = source;
}
}
/**
* Notify this object that it has been deregistered as a listener with
* a source for named event. This object is responsible
* for recording this fact.
*
* @param eventName the event
* @param source the source with which this object has been registered as
* a listener
*/
public void disconnectionNotification(String eventName, Object source) {
m_listenee = null;
}
/**
* Describe enumerateRequests
method here.
*
* @return an Enumeration
value
*/
public Enumeration enumerateRequests() {
Vector newVector = new Vector(0);
newVector.addElement("Show chart");
return newVector.elements();
}
/**
* Describe performRequest
method here.
*
* @param request a String
value
* @exception IllegalArgumentException if an error occurs
*/
public void performRequest(String request) {
if (request.compareTo("Show chart") == 0) {
showChart();
} else {
throw new
IllegalArgumentException(request
+ " not supported (StripChart)");
}
}
/**
* Tests out the StripChart from the command line
*
* @param args ignored
*/
public static void main(String [] args) {
try {
final javax.swing.JFrame jf =
new javax.swing.JFrame("Weka Knowledge Flow : StipChart");
jf.getContentPane().setLayout(new BorderLayout());
final StripChart jd = new StripChart();
jf.getContentPane().add(jd, BorderLayout.CENTER);
jf.addWindowListener(new java.awt.event.WindowAdapter() {
public void windowClosing(java.awt.event.WindowEvent e) {
jf.dispose();
System.exit(0);
}
});
jf.pack();
jf.setVisible(true);
jd.showChart();
Random r = new Random(1);
for (int i = 0; i < 1020; i++) {
double [] pos = new double[1];
pos[0] = r.nextDouble();
jd.acceptDataPoint(pos);
}
System.err.println("Done sending data");
} catch (Exception ex) {
ex.printStackTrace();
System.err.println(ex.getMessage());
}
}
}