/* 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; } }