All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hfg.math.Histogram Maven / Gradle / Ivy

There is a newer version: 20240423
Show newest version
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; } }