/*
Copyright 2006 by Sean Luke and George Mason University
Licensed under the Academic Free License version 3.0
See the file "LICENSE" for more information
*/
package sim.util.gui;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
/** <p>A very simple histogram class. Displays values as different colors left to right.
Tooltips pop up describing the actual buckets and their densities.
<p>This class can be used to describe any one-dimensional array of doubles: just provide
an array of doubles to setBuckets and it'll show them on-screen. If you change the doubles in that
array, no need to call setBuckets again: as soon as a repaint occurs, MiniHistogram will update them.
You can also provide a
set of bucket labels, one for each bucket, which get popped up in the tooltip along with the current
bucket value.
<p>As the class was created to describe histograms in particular, it has two functions
(makeBuckets and makeBucketLabels) which do just that.
Sample usage. The following values are placed into a histogram of four buckets.
<tt><pre>
MiniHistogram m = new MiniHistogram();
double[] d = new double[] {1, 1, 1, 1, 2, 2, 5, 1, 2, 3, 1, 2, 1, 1, 5, 4};
m.setBuckets(m.makeBuckets(d, 4, 1, 5, false));
m.setBucketLabels(m.makeBucketLabels(4, 1, 5));
</pre></tt>
*/
public class MiniHistogram extends JComponent
{
static JLabel DEFAULT_SIZE_COMPARISON = new JLabel("X");
double[] buckets;
String[] labels;
public MiniHistogram()
{
setBuckets(new double[0]);
addMouseListener(adapter);
addMouseMotionListener(motionAdapter);
setBackground(DEFAULT_SIZE_COMPARISON.getBackground());
}
public MiniHistogram(double[] buckets, String[] labels)
{
this();
setBucketsAndLabels(buckets,labels);
setBackground(DEFAULT_SIZE_COMPARISON.getBackground());
setOpaque(true);
}
public Dimension getPreferredSize()
{
return DEFAULT_SIZE_COMPARISON.getPreferredSize();
}
public Dimension getMinimumSize()
{
return DEFAULT_SIZE_COMPARISON.getMinimumSize();
}
/** Sets the displayed bucket array. If you change values in the provided array, then
MiniHistogram will update itself appropriately on next repaint. */
public void setBuckets(double[] buckets)
{
if (buckets == null) buckets = new double[0];
this.buckets = buckets;
repaint();
}
/** Sets labels for the buckets provided in setBuckets. */
public void setBucketLabels(String[] labels)
{
this.labels = labels;
}
public void setBucketsAndLabels(double[] buckets, String[] labels)
{
setBuckets(buckets);
setBucketLabels(labels);
}
MouseMotionAdapter motionAdapter = new MouseMotionAdapter()
{
public void mouseMoved(MouseEvent event)
{
String s = null;
if (buckets !=null)
{
int x = (int)((event.getX() * buckets.length) / (double)(getBounds().width));
if (labels != null && x < labels.length)
s = "<html><font size=\"-1\" face=\"" + getFont().getFamily() + "\">" +
"Bucket: " + x + "<br>Range: " + labels[x] + "<br>Value: " + buckets[x] +
"</font></html>";
else if (buckets != null && buckets.length != 0)
s = "<html><font size=\"-1\" face=\"" + getFont().getFamily() + "\">" +
"Bucket: " + x + "<br>>Value: " + buckets[x] +
"</font></html>";
else s=null;
}
if ((s != null) && !s.equalsIgnoreCase(getToolTipText()))
setToolTipText(s);
}
};
// the purpose of this adapter is to force the tooltip manager to turn on
// its initial delay
MouseAdapter adapter = new MouseAdapter()
{
int initialDelay;
int dismissDelay;
int reshowDelay;
public void mouseEntered(MouseEvent event)
{
initialDelay = ToolTipManager.sharedInstance().getInitialDelay();
ToolTipManager.sharedInstance().setInitialDelay(0);
dismissDelay = ToolTipManager.sharedInstance().getDismissDelay();
ToolTipManager.sharedInstance().setDismissDelay(Integer.MAX_VALUE);
reshowDelay = ToolTipManager.sharedInstance().getReshowDelay();
ToolTipManager.sharedInstance().setReshowDelay(0);
}
public void mouseExited(MouseEvent event)
{
ToolTipManager.sharedInstance().setInitialDelay(initialDelay);
ToolTipManager.sharedInstance().setDismissDelay(dismissDelay);
ToolTipManager.sharedInstance().setReshowDelay(reshowDelay);
}
};
public void paintComponent(final Graphics graphics)
{
int len = 0;
if (buckets != null) len = buckets.length;
if (len==0) return; // don't bother drawing
Rectangle bounds = getBounds();
graphics.setColor(getBackground());
graphics.fillRect(0,0,bounds.width,bounds.height);
int height = bounds.height - 2;
if (height <= 0) return;
graphics.setColor(getForeground());
// find maxbucket, minbucket
double maxbucket = buckets[0];
double minbucket = buckets[0];
for(int i=1;i<len;i++)
{
if (buckets[i] < minbucket) minbucket = buckets[i];
if (buckets[i] > maxbucket) maxbucket = buckets[i];
}
if (maxbucket==minbucket)
{
graphics.fillRect(0,0,bounds.width,height);
return;
}
for(int i=0;i<len;i++)
{
int x0 = (int)(bounds.width / (double)len * i);
int x1 = (int)(bounds.width / (double)len * (i+1));
int y0 = 0;
int y1;
if (buckets[i]==Double.POSITIVE_INFINITY) y1 = height;
else if (buckets[i]!=buckets[i]) y1 = 0; // it's NaN
else y1 = (int)(height * ((buckets[i] - minbucket) / (double)(maxbucket-minbucket)));
// flip the y's
y0 = height - y0;
y1 = height - y1;
graphics.fillRect(x0+1,y1+1,x1-x0+1,y0-y1 + 1);
}
}
/** Generates a set of <i>numBuckets</i> bucket labels appropriate for use in a histogram.
The values <i>numBuckets</i>, <i>min</i>, and <i>max</i> should be the same values you had
provided to makeBuckets(...). Pass the resulting array into the setBuckets(...) function.*/
public static String[] makeBucketLabels(int numBuckets, double min, double max, boolean logScale)
{
String[] s = new String[numBuckets];
if (min>max) {double tmp = min; min = max; max = tmp;} // duh, stupid user
if (min==max) { s[0] = "["+min+"..."+max+"]"; for(int x=1;x<s.length;x++) s[x] = ""; return s;} // duh, stupider user
else if (logScale)
{
min = Math.log(min);
max = Math.log(max);
for(int x=0;x<s.length;x++)
s[x] = "[" + Math.exp((x/(double)numBuckets)*(max-min) + min) +
"..." + Math.exp(((x+1)/(double)numBuckets)*(max-min) + min) +
(x==s.length-1 ? "]" : ")");
}
else
for(int x=0;x<s.length;x++)
s[x] = "[" + ((x/(double)numBuckets)*(max-min) + min) +
"..." + (((x+1)/(double)numBuckets)*(max-min) + min) +
(x==s.length-1 ? "]" : ")");
return s;
}
/*
public static double[] makeBucketPositions(int numBuckets, double min, double max, boolean logScale)
{
double[] s = new double[numBuckets];
if (min>=max) { for(int x=0;x<s.length;x++) s[x] = max; return s;} // duh, stupider user
else if (logScale)
{
min = Math.log(min);
max = Math.log(max);
for(int x=0;x<s.length;x++)
s[x] = (Math.exp((x/(double)numBuckets)*(max-min) + min) +
Math.exp(((x+1)/(double)numBuckets)*(max-min) + min)) / 2;
}
else
for(int x=0;x<s.length;x++)
s[x] = (((x/(double)numBuckets)*(max-min) + min) +
(((x+1)/(double)numBuckets)*(max-min) + min)) / 2;
return s;
}
*/
/** Returns the minimum over the provided vals. You might use this to set the minimum in makeBuckets
if you don't have a prescribed minimum */
public static double minimum(double[] vals)
{
double min = Double.POSITIVE_INFINITY;
for(int i=0;i<vals.length;i++) // gather minimum
if (min > vals[i])
min = vals[i];
return min;
}
/** Returns the minimum over the provided vals. You might use this to set the minimum in makeBuckets
if you don't have a prescribed minimum */
public static double maximum(double[] vals)
{
double max = Double.NEGATIVE_INFINITY;
for(int i=0;i<vals.length;i++) // gather maximum
if (max < vals[i])
max = vals[i];
return max;
}
/** Generates a set of <i>numBuckets</i> buckets describing a histogram over the provided values in <i>vals</i>.
<i>min</i> and <i>max</i> describe the ends of the histogram, inclusive: values outside those ranges are discarded.
You can tell the histogram to bucket based on a log scale if you like (min and max are computed prior to log)),
and in this case all values less than 0 are discarded as well. Pass the resulting array into the setBuckets(...) function.
*/
public static double[] makeBuckets(double[] vals, int numBuckets, double min, double max, boolean logScale)
{
double[] b = new double[numBuckets];
if (vals == null || numBuckets == 0) return b; // duh, stupid user
if (logScale) { min = Math.log(min); max = Math.log(max); }
if (min>max) {double tmp = min; min = max; max = tmp;} // duh, stupid user
else if (min==max) { b[0] += vals.length; return b; } // duh, stupider user
int count = 0;
for(int x=0;x<vals.length;x++)
{
double v = vals[x];
if (logScale)
{
if (v<0) continue;
v = Math.log(v);
}
if (v < min || v > max) continue;
int bucketnum = (int)((v - min) * numBuckets / (max-min));
if (bucketnum >= numBuckets) bucketnum = numBuckets-1;
b[bucketnum]++;
count++;
}
if (count != 0)
for(int x=0;x<b.length;x++)
b[x] /= count;
return b;
}
}