com.hfg.math.Histogram Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of com_hfg Show documentation
Show all versions of com_hfg Show documentation
com.hfg xml, html, svg, and bioinformatics utility library
package com.hfg.math;
import java.awt.*;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.hfg.graphics.TextUtil;
import com.hfg.graphics.units.GfxUnits;
import com.hfg.html.attribute.HTMLColor;
import com.hfg.svg.SVG;
import com.hfg.svg.SvgGroup;
import com.hfg.svg.SvgRect;
import com.hfg.util.StringUtil;
import com.hfg.util.collection.OrderedMap;
//------------------------------------------------------------------------------
/**
Generic histogram for bucketing data into ranges.
@author J. Alex Taylor, hairyfatguy.com
*/
//------------------------------------------------------------------------------
// com.hfg Library
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
// [email protected]
//------------------------------------------------------------------------------
public class Histogram
{
private S mBinSize;
private SimpleSampleStats mStats = new SimpleSampleStats();
private Map> mRangeLookupMap = new HashMap<>();
private Map, Counter> mCounterMap = new HashMap<>();
//###########################################################################
// CONSTRUCTORS
//###########################################################################
//--------------------------------------------------------------------------
public Histogram(S inBinSize)
{
mBinSize = inBinSize;
}
//###########################################################################
// PUBLIC METHODS
//###########################################################################
//--------------------------------------------------------------------------
public S getBinSize()
{
return mBinSize;
}
//--------------------------------------------------------------------------
public SimpleSampleStats getStats()
{
return mStats;
}
//--------------------------------------------------------------------------
public void addData(S[] inValues)
{
if (inValues != null)
{
for (S value : inValues)
{
addData(value);
}
}
}
//--------------------------------------------------------------------------
public void addData(S inValue)
{
mStats.add(inValue);
// First, determine the range for the value
// Round the value down to the nearest range starting point
float binStart = (float) Math.floor(inValue.floatValue()/mBinSize.floatValue()) * mBinSize.floatValue();
Range range = mRangeLookupMap.get(binStart);
if (null == range)
{
range = new Range<>();
range.setStart(fromFloat(binStart));
range.setEnd(fromFloat(binStart + (mBinSize instanceof Integer ? -1 : 0) + mBinSize.floatValue()));
mRangeLookupMap.put(binStart, range);
}
Counter counter = mCounterMap.get(range);
if (null == counter)
{
counter = new Counter();
mCounterMap.put(range, counter);
}
counter.increment();
}
//--------------------------------------------------------------------------
public void addData(Map inValueCountMap)
{
mStats.addAll((Map) inValueCountMap);
for (S value : inValueCountMap.keySet())
{
Integer count = inValueCountMap.get(value);
if (count != null
&& count != 0)
{
// First, determine the range for the value
// Round the value down to the nearest range starting point
float binStart = (float) Math.floor(value.floatValue() / mBinSize.floatValue()) * mBinSize.floatValue();
Range range = mRangeLookupMap.get(binStart);
if (null == range)
{
range = new Range<>();
range.setStart(fromFloat(binStart));
range.setEnd(fromFloat(binStart + (mBinSize instanceof Integer ? -1 : 0) + mBinSize.floatValue()));
mRangeLookupMap.put(binStart, range);
}
Counter counter = mCounterMap.get(range);
if (null == counter)
{
counter = new Counter();
mCounterMap.put(range, counter);
}
counter.add(count);
}
}
}
//--------------------------------------------------------------------------
public Map, Counter> getOrderedRangeMap()
{
// Start with the existing ranges.
List> orderedRanges = getOrderedRanges();
// Flesh out the list with missing ranges
for (int i = 1; i < orderedRanges.size(); i++)
{
S expectedStart = fromFloat(orderedRanges.get(i - 1).getStart().floatValue() + getBinSize().floatValue());
if (orderedRanges.get(i).getStart().floatValue() - expectedStart.floatValue() >= 0.9f * getBinSize().floatValue())
{
Range newRange = new Range<>();
newRange.setStart(expectedStart);
newRange.setEnd(fromFloat(expectedStart.floatValue() + (mBinSize instanceof Integer ? -1 : 0) + getBinSize().floatValue()));
orderedRanges.add(i--, newRange);
}
}
Map, Counter> orderedMap = new OrderedMap<>(orderedRanges.size());
for (Range range : orderedRanges)
{
orderedMap.put(range, mCounterMap.get(range));
}
return orderedMap;
}
//--------------------------------------------------------------------------
public Range getOverallRange()
{
List> orderedRanges = getOrderedRanges();
Range overallRange = new Range<>();
overallRange.setStart(orderedRanges.get(0).getStart());
overallRange.setEnd(orderedRanges.get(orderedRanges.size() - 1).getEnd());
return overallRange;
}
//--------------------------------------------------------------------------
public int getMaxCount()
{
int maxCount = 0;
for (Counter counter : mCounterMap.values())
{
if (counter.intValue() > maxCount)
{
maxCount = counter.intValue();
}
}
return maxCount;
}
//--------------------------------------------------------------------------
public SVG toSVG(HistogramSvgSettings inSettings)
{
int tickHeight = 6;
int labelOffset = 3;
String rangeLabelFormatString = inSettings.getRangeLabelFormatString();
if (null == rangeLabelFormatString)
{
rangeLabelFormatString = "%f";
}
SVG svg = new SVG();
svg.setFont(inSettings.getFont());
Range overallRange = getOverallRange();
Map, Counter> orderedRangeMap = getOrderedRangeMap();
boolean singleIntBin = (getBinSize() instanceof Integer && getBinSize().floatValue() == 1);
float xScalingFactor = inSettings.calculateXScalingFactor(singleIntBin ? new Range<>(0, orderedRangeMap.size() - 1) : overallRange);
float yScalingFactor = inSettings.calculateYScalingFactor(new Range<>(0, getMaxCount()));
int xStartPx = (int) inSettings.calculateXStart().to(GfxUnits.pixels);
int xEndPx = (int) inSettings.calculateXEnd().to(GfxUnits.pixels);
int yTopPx = (int) inSettings.calculateYTop().to(GfxUnits.pixels);
int yBottomPx = (int) inSettings.calculateYBottom().to(GfxUnits.pixels);
// Generate the axes
SvgGroup axes = svg.addGroup().setId("Axes").addStyle("stroke:#000000");
// X-axis
SvgGroup xAxis = axes.addGroup().setId("X-axis");
xAxis.addLine(new Point(xStartPx, yBottomPx), new Point(xEndPx, yBottomPx));
int count = 0;
for (Map.Entry, Counter> bin : orderedRangeMap.entrySet())
{
count++;
int x;
if (singleIntBin)
{
// Special case where the bins are single integers
x = (int) (xStartPx + (count - 0.5) * xScalingFactor);
}
else
{
x = (int) (xStartPx + ((bin.getKey().getStart().floatValue() - overallRange.getStart().floatValue()) * xScalingFactor));
}
xAxis.addLine(new Point(x, yBottomPx), new Point(x, yBottomPx + tickHeight));
String label = String.format(rangeLabelFormatString, bin.getKey().getStart().floatValue());
Rectangle2D textBoundBox = TextUtil.getStringBaselineRect(label, inSettings.getFont());
float labelX = x - (int) textBoundBox.getWidth() / 2;
float labelY = (float) (yBottomPx + tickHeight + labelOffset + textBoundBox.getHeight() / 2 + textBoundBox.getWidth() / 2);
Point2D labelLoc = new Point2D.Float(labelX, labelY);
Point2D rotationCenter = new Point2D.Float(x, (float) (labelY - textBoundBox.getHeight() / 2));
xAxis.addText(label, inSettings.getFont(), labelLoc)
.setTransform("rotate(-90 " + rotationCenter.getX() + "," + rotationCenter.getY() + ")");
// Last range?
if (count == orderedRangeMap.size()
&& ! singleIntBin)
{
float xEnd = bin.getKey().getEnd().floatValue() + (mBinSize instanceof Integer ? 1 : 0);
x = (int) (xStartPx + ((xEnd - overallRange.getStart().floatValue()) * xScalingFactor));
xAxis.addLine(new Point(x, yBottomPx), new Point(x, yBottomPx + tickHeight));
label = String.format(rangeLabelFormatString, xEnd);
textBoundBox = TextUtil.getStringBaselineRect(label, inSettings.getFont());
// Put the label down far enough so that when we center rotate it 90deg, it will line up with the tick mark
labelX = x - (float) textBoundBox.getWidth() / 2;
labelY = (float) (yBottomPx + tickHeight + labelOffset + textBoundBox.getHeight() / 2 + textBoundBox.getWidth() / 2);
labelLoc = new Point2D.Float(labelX, labelY);
rotationCenter = new Point2D.Float((float) x, (float) (labelY - textBoundBox.getHeight() / 2));
xAxis.addText(label, inSettings.getFont(), labelLoc)
.setTransform("rotate(-90 " + rotationCenter.getX() + "," + rotationCenter.getY() + ")");
}
}
// Y-axis
// axes.addLine(new Point(xStartPx, yTopPx), new Point(xStartPx, yBottomPx));
// Add the bars
SvgGroup bars = svg.addGroup();
SvgGroup labels = svg.addGroup();
orderedRangeMap.entrySet().iterator().next();
int barWidth = (int) (getBinSize().floatValue() * xScalingFactor * 0.85f);
String barClass = inSettings.getBarStyleClass();
for (Map.Entry, Counter> bin : orderedRangeMap.entrySet())
{
int x = xStartPx + (int) ((bin.getKey().getStart().floatValue() - overallRange.getStart().floatValue()) * xScalingFactor + 0.075 * barWidth);
int height = bin.getValue() != null ? (int) (bin.getValue().intValue() * yScalingFactor) : 0;
int y = (int) (yBottomPx - height);
String label = (bin.getValue() != null ? bin.getValue().intValue() : 0) + "";
String rangeString;
if (singleIntBin)
{
rangeString = String.format(inSettings.getRangeLabelFormatString(),
bin.getKey().getStart().floatValue());
}
else
{
rangeString = String.format(inSettings.getRangeLabelFormatString() + ", " + inSettings.getRangeLabelFormatString(),
bin.getKey().getStart().floatValue(),
bin.getKey().getEnd().floatValue());
}
SvgRect bar = bars.addRect(new Rectangle(x, y, barWidth, height));
bar.setAttribute("data-range", rangeString);
bar.setTitle("[" + rangeString + "]: " + label);
if (StringUtil.isSet(barClass))
{
bar.addClass(barClass);
}
else
{
bar.addStyle("fill:#0000ff;")
.setStroke(HTMLColor.BLACK);
}
// Put the count (label) over the bar
Rectangle2D textBoundBox = TextUtil.getStringBaselineRect(label, inSettings.getFont());
Point2D labelLoc = new Point(x + (int) (barWidth - textBoundBox.getWidth())/2, (int) (y - textBoundBox.getWidth()/2));
Point2D rotationCenter = new Point((int) (labelLoc.getX() + textBoundBox.getWidth()/2),
(int) (labelLoc.getY() - textBoundBox.getHeight()/2));
labels.addText(label, inSettings.getFont(), labelLoc)
.setTransform("rotate(-90 " + rotationCenter.getX() + "," + rotationCenter.getY() + ")");
}
svg.setHeight((int) inSettings.getHeight().to(GfxUnits.pixels));
svg.setWidth((int) inSettings.getWidth().to(GfxUnits.pixels));
return svg;
}
//###########################################################################
// PRIVATE METHODS
//###########################################################################
//--------------------------------------------------------------------------
public List> getOrderedRanges()
{
// Start with the existing ranges.
List> orderedRanges = new ArrayList<>(mCounterMap.keySet());
Collections.sort(orderedRanges);
return orderedRanges;
}
//--------------------------------------------------------------------------
private S fromFloat(Float inValue)
{
S value = null;
S binSize = getBinSize();
if (binSize instanceof Integer)
{
value = (S) Integer.valueOf(inValue.intValue());
}
else if (binSize instanceof Float)
{
value = (S) inValue;
}
else if (binSize instanceof Double)
{
value = (S) Double.valueOf(inValue.doubleValue());
}
else if (binSize instanceof Long)
{
value = (S) Long.valueOf(inValue.longValue());
}
else if (binSize instanceof BigDecimal)
{
value = (S) new BigDecimal(inValue.doubleValue());
}
return value;
}
}