package prefuse.action.layout;
import java.awt.geom.Rectangle2D;
import java.util.Arrays;
import java.util.Iterator;
import prefuse.Constants;
import prefuse.data.Table;
import prefuse.data.query.NumberRangeModel;
import prefuse.util.ArrayLib;
import prefuse.util.MathLib;
import prefuse.util.PrefuseLib;
import prefuse.util.ui.ValuedRangeModel;
import prefuse.visual.VisualItem;
/**
* Layout Action that computes a stacked area chart, in which a series of
* data values are consecutively stacked on top of each other.
*
* @author <a href="http://jheer.org">jeffrey heer</a>
*/
public class StackedAreaChart extends Layout {
private String m_field;
private String m_start;
private String m_end;
private String[] columns;
private double[] baseline;
private double[] peaks;
private float[] poly;
private double m_padding = 0.05;
private float m_threshold;
private Rectangle2D bounds;
private int m_orientation = Constants.ORIENT_BOTTOM_TOP;
private boolean m_horiz = false;
private boolean m_top = false;
private boolean m_norm = false;
private NumberRangeModel m_model;
/**
* Create a new StackedAreaChart.
* @param group the data group to layout
* @param field the data field in which to store computed polygons
* @param columns the various data fields, in sorted order, that
* should be referenced for each consecutive point of a stack layer
*/
public StackedAreaChart(String group, String field, String[] columns) {
this(group, field, columns, 1.0);
}
/**
* Create a new StackedAreaChart.
* @param group the data group to layout
* @param field the data field in which to store computed polygons
* @param columns the various data fields, in sorted order, that
* should be referenced for each consecutive point of a stack layer
* @param threshold height threshold under which stacks should not
* be made visible.
*/
public StackedAreaChart(String group, String field, String[] columns,
double threshold)
{
super(group);
this.columns = columns;
baseline = new double[columns.length];
peaks = new double[columns.length];
poly = new float[4*columns.length];
m_field = field;
m_start = PrefuseLib.getStartField(field);
m_end = PrefuseLib.getEndField(field);
setThreshold(threshold);
m_model = new NumberRangeModel(0,1,0,1);
}
// ------------------------------------------------------------------------
/**
* Set the data columns used to compute the stacked layout
* @param cols the various data fields, in sorted order, that
* should be referenced for each consecutive point of a stack layer
*/
public void setColumns(String[] cols) {
columns = cols;
}
/**
* Sets if the stacks are normalized, such that each
* column is independently scaled.
* @param b true to normalize, false otherwise
*/
public void setNormalized(boolean b) {
m_norm = b;
}
/**
* Indicates if the stacks are normalized, such that each
* column is independently scaled.
* @return true if normalized, false otherwise
*/
public boolean isNormalized() {
return m_norm;
}
/**
* Gets the percentage of the layout bounds that should be reserved for
* empty space at the top of the stack.
* @return the padding percentage
*/
public double getPaddingPercentage() {
return m_padding;
}
/**
* Sets the percentage of the layout bounds that should be reserved for
* empty space at the top of the stack.
* @param p the padding percentage to use
*/
public void setPaddingPercentage(double p) {
if ( p < 0 || p > 1 )
throw new IllegalArgumentException(
"Illegal padding percentage: " + p);
m_padding = p;
}
/**
* Get the minimum height threshold under which stacks should not be
* made visible.
* @return the minimum height threshold for visibility
*/
public double getThreshold() {
return m_threshold;
}
/**
* Set the minimum height threshold under which stacks should not be
* made visible.
* @param threshold the minimum height threshold for visibility to use
*/
public void setThreshold(double threshold) {
m_threshold = (float)threshold;
}
/**
* Get the range model describing the range occupied by the value
* stack.
* @return the stack range model
*/
public ValuedRangeModel getRangeModel() {
return m_model;
}
/**
* Returns the orientation of this layout. One of
* {@link Constants#ORIENT_BOTTOM_TOP} (to grow bottom-up),
* {@link Constants#ORIENT_TOP_BOTTOM} (to grow top-down),
* {@link Constants#ORIENT_LEFT_RIGHT} (to grow left-right), or
* {@link Constants#ORIENT_RIGHT_LEFT} (to grow right-left).
* @return the orientation of this layout
*/
public int getOrientation() {
return m_orientation;
}
/**
* Sets the orientation of this layout. Must be one of
* {@link Constants#ORIENT_BOTTOM_TOP} (to grow bottom-up),
* {@link Constants#ORIENT_TOP_BOTTOM} (to grow top-down),
* {@link Constants#ORIENT_LEFT_RIGHT} (to grow left-right), or
* {@link Constants#ORIENT_RIGHT_LEFT} (to grow right-left).
* @param orient the desired orientation of this layout
* @throws IllegalArgumentException if the orientation value
* is not a valid value
*/
public void setOrientation(int orient) {
if ( orient != Constants.ORIENT_TOP_BOTTOM &&
orient != Constants.ORIENT_BOTTOM_TOP &&
orient != Constants.ORIENT_LEFT_RIGHT &&
orient != Constants.ORIENT_RIGHT_LEFT) {
throw new IllegalArgumentException(
"Invalid orientation value: "+orient);
}
m_orientation = orient;
m_horiz = (m_orientation == Constants.ORIENT_LEFT_RIGHT ||
m_orientation == Constants.ORIENT_RIGHT_LEFT);
m_top = (m_orientation == Constants.ORIENT_TOP_BOTTOM ||
m_orientation == Constants.ORIENT_LEFT_RIGHT);
}
// TODO: support externally driven range specification (i.e. stack zooming)
// public void setRangeModel(NumberRangeModel model) {
// m_model = model;
// }
// ------------------------------------------------------------------------
/**
* @see prefuse.action.Action#run(double)
*/
public void run(double frac) {
bounds = getLayoutBounds();
Arrays.fill(baseline, 0);
// get the orientation specifics sorted out
float min = (float)(m_horiz?bounds.getMaxY() :bounds.getMinX());
float hgt = (float)(m_horiz?bounds.getWidth():bounds.getHeight());
int xbias = (m_horiz ? 1 : 0);
int ybias = (m_horiz ? 0 : 1);
int mult = m_top ? 1 : -1;
float inc = (float) (m_horiz ? (bounds.getMinY()-bounds.getMaxY())
: (bounds.getMaxX()-bounds.getMinX()));
inc /= columns.length-1;
int len = columns.length;
// perform first walk to compute max values
double maxValue = getPeaks();
float b = (float)(m_horiz ? (m_top?bounds.getMinX():bounds.getMaxX())
: (m_top?bounds.getMinY():bounds.getMaxY()));
Arrays.fill(baseline, b);
m_model.setValueRange(0, maxValue, 0, maxValue);
// perform second walk to compute polygon layout
Table t = (Table)m_vis.getGroup(m_group);
Iterator iter = t.tuplesReversed();
while ( iter.hasNext() ) {
VisualItem item = (VisualItem)iter.next();
if ( !item.isVisible() ) continue;
float height = 0;
for ( int i=len; --i >= 0; ) {
poly[2*(len-1-i)+xbias] = min + i*inc;
poly[2*(len-1-i)+ybias] = (float)baseline[i];
}
for ( int i=0; i<columns.length; ++i ) {
int base = 2*(len+i);
double value = item.getDouble(columns[i]);
baseline[i] += mult * hgt *
MathLib.linearInterp(value,0,peaks[i]);
poly[base+xbias] = min + i*inc;
poly[base+ybias] = (float)baseline[i];
height = Math.max(height,
Math.abs(poly[2*(len-1-i)+ybias]-poly[base+ybias]));
}
if ( height < m_threshold ) {
item.setVisible(false);
}
setX(item, null, 0);
setY(item, null, 0);
setPolygon(item, poly);
}
}
private double getPeaks() {
double sum = 0;
// first, compute max value of the current data
Arrays.fill(peaks, 0);
Iterator iter = m_vis.visibleItems(m_group);
while ( iter.hasNext() ) {
VisualItem item = (VisualItem)iter.next();
for ( int i=0; i<columns.length; ++i ) {
double val = item.getDouble(columns[i]);
peaks[i] += val;
sum += val;
}
}
double max = ArrayLib.max(peaks);
// update peaks array as needed
if ( !m_norm ) {
Arrays.fill(peaks, max);
}
// adjust peaks to include padding space
if ( !m_norm ) {
for ( int i=0; i<peaks.length; ++i ) {
peaks[i] += m_padding * peaks[i];
}
max += m_padding*max;
}
// return max range value
if ( m_norm ) {
max = 1.0;
}
if ( Double.isNaN(max) )
max = 0;
return max;
}
/**
* Sets the polygon values for a visual item.
*/
private void setPolygon(VisualItem item, float[] poly) {
float[] a = getPolygon(item, m_field);
float[] s = getPolygon(item, m_start);
float[] e = getPolygon(item, m_end);
System.arraycopy(a, 0, s, 0, a.length);
System.arraycopy(poly, 0, a, 0, poly.length);
System.arraycopy(poly, 0, e, 0, poly.length);
item.setValidated(false);
}
/**
* Get the polygon values for a visual item.
*/
private float[] getPolygon(VisualItem item, String field) {
float[] poly = (float[])item.get(field);
if ( poly == null || poly.length < 4*columns.length ) {
// get oriented
int len = columns.length;
float inc = (float) (m_horiz?(bounds.getMinY()-bounds.getMaxY())
:(bounds.getMaxX()-bounds.getMinX()));
inc /= len-1;
float max = (float)
(m_horiz ? (m_top?bounds.getMaxX():bounds.getMinX())
: (m_top?bounds.getMinY():bounds.getMaxY()));
float min = (float)(m_horiz?bounds.getMaxY():bounds.getMinX());
int bias = (m_horiz ? 1 : 0);
// create polygon, populate default values
poly = new float[4*len];
Arrays.fill(poly, max);
for ( int i=0; i<len; ++i ) {
float x = i*inc + min;
poly[2*(len+i) +bias] = x;
poly[2*(len-1-i)+bias] = x;
}
item.set(field, poly);
}
return poly;
}
} // end of class StackedAreaChart