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

com.barrybecker4.ui.renderers.HistogramRenderer.scala Maven / Gradle / Ivy

The newest version!
/* Copyright by Barry G. Becker, 2015 2018. Licensed under MIT License: http://www.opensource.org/licenses/MIT */
package com.barrybecker4.ui.renderers

import com.barrybecker4.common.app.AppContext
import com.barrybecker4.common.format.DefaultNumberFormatter
import com.barrybecker4.common.format.FormatUtil._
import com.barrybecker4.common.format.INumberFormatter
import com.barrybecker4.math.function.InvertibleFunction
import com.barrybecker4.math.function.LinearFunction
import HistogramRenderer._
import java.awt._
import com.barrybecker4.ui.renderers.model.HistogramModel


object HistogramRenderer {
  private val BACKGROUND_COLOR = new Color(255, 255, 255)
  private val BAR_COLOR = new Color(160, 120, 255)
  private val BAR_BORDER_COLOR = new Color(0, 0, 0)
  private val FONT = new Font("Sanserif", Font.PLAIN, 12)
  private val BOLD_FONT = new Font("Sanserif", Font.BOLD, 12)
  private val DEFAULT_LABEL_WIDTH = 30
  private val MARGIN: Int = 24
  private val TICK_LENGTH = 2
  private val NARROW_X_THRESH = 390
}

/**
  * This class renders a histogram.
  * The histogram is defined as an array of integers.
  *
  * @param data            the array to hold counts for each x axis position. y values for every point on the x axis.
  * @param xFunction       a way to scale the values on the x axis.
  *                        This function takes an x value in the domain space and maps it to a bin index location.
  * @param integralXValues if true, then the x values will assumed to be only equal to the low bin cutPoint value.
  *                        This implies that the the median only be one of those low bin cutPoint values.
  * @author Barry Becker
  */
class HistogramRenderer(val data: Array[Int],
                        val xFunction: InvertibleFunction,
                        val integralXValues: Boolean = false) {

  private val model = new HistogramModel(data, xFunction, integralXValues)
  private var width = 0
  private var height = 0
  private var maxNumLabels = 0
  private var barWidth: Float = 0.0f
  private var zeroXPos = 0
  private var formatter: INumberFormatter = new DefaultNumberFormatter
  private var maxLabelWidth = DEFAULT_LABEL_WIDTH

  /** Constructor that starts at x=0 and assumes no scaling ont he x axis.
    * @param data the array to hold counts for each x axis position.
    */
  def this(data: Array[Int]) = {
    this(data, new LinearFunction(1.0))
  }

  def setSize(width: Int, height: Int): Unit = {
    this.width = width
    this.height = height
    maxNumLabels = this.width / maxLabelWidth
    barWidth = (this.width - 2.0F * MARGIN) / model.numBars
    zeroXPos = calcZeroPos()
  }

  def increment(xValue: Double): Unit = model.increment(xValue)

  /** Provides customer formatting for the x axis values.
    * @param formatter a way to format the x axis values
    */
  def setXFormatter(formatter: INumberFormatter): Unit = { this.formatter = formatter }

  /** The larger this is, the fewer equally spaced x labels.
    * @param maxLabelWidth max width of x labels.
    */
  def setMaxLabelWidth(maxLabelWidth: Int): Unit = { this.maxLabelWidth = maxLabelWidth }

  /** draw the histogram graph */
  def paint(g: Graphics): Unit = {
    if (g == null) return
    val g2 = g.asInstanceOf[Graphics2D]
    g2.setFont(FONT)
    val maxHeight = model.getMaxHeight
    val scale = (height - 2.0 * MARGIN) / maxHeight
    clearBackground(g2)
    var xpos: Float = MARGIN.toFloat
    var xIdx = 0
    for (value <- data) {
      drawBar(g2, scale, xpos, xIdx, value)
      xIdx += 1
      xpos += barWidth
    }
    drawDecoration(g2, maxHeight)
  }

  private def drawDecoration(g2: Graphics2D, maxHeight: Int): Unit = {
    val width = (barWidth * model.numBars).toInt
    drawAxes(g2, maxHeight, width)
    drawVerticalMarkers(g2, width)
  }

  private def drawAxes(g2: Graphics2D, maxHeight: Int, width: Int): Unit = { // left y axis
    g2.drawLine(MARGIN - 1, height - MARGIN, MARGIN - 1, MARGIN)
    // x axis
    g2.drawLine(MARGIN - 1, height - MARGIN - 1, MARGIN - 1 + width, height - MARGIN - 1)
    val numTrials = formatNumber(model.sum)
    val yOffset = 8

    val trialsX = width - (if (width > NARROW_X_THRESH) 230 + 0.2 * width else 210).toInt
    val meanX = width - (if (width > NARROW_X_THRESH) 170 + 0.1 * width else 80).toInt
    g2.drawString(AppContext.getLabel("HEIGHT") + " = " + formatNumber(maxHeight), MARGIN / 3, MARGIN - yOffset)
    if (width > 0.7 * NARROW_X_THRESH) {
      g2.drawString(AppContext.getLabel("NUM_TRIALS") + " = " + numTrials, trialsX, MARGIN - yOffset)
    }
    g2.setFont(BOLD_FONT)
    if (width > 0.4 * NARROW_X_THRESH) {
      g2.drawString(AppContext.getLabel("MEAN") + " = " + formatNumber(model.mean), meanX, MARGIN - yOffset)
    }
    if (width > NARROW_X_THRESH) {
      val median = model.calcMedian(model.calcMedianPos)
      g2.drawString(AppContext.getLabel("MEDIAN") + " = " + formatNumber(median), width - 90, MARGIN - yOffset)
    }
    g2.setFont(FONT)
  }

  private def drawVerticalMarkers(g2: Graphics2D, width: Double): Unit = {
    drawMeanLine(g2, width)
    drawMedianLine(g2, width)
    drawZeroLine(g2, width)
  }

  private def drawMeanLine(g2: Graphics2D, width: Double): Unit = {
    val meanXpos = (MARGIN + width * xFunction.getValue(model.mean) / model.numBars + barWidth / 2).toInt
    g2.drawLine(meanXpos, height - MARGIN, meanXpos, MARGIN)
    g2.drawString(AppContext.getLabel("MEAN"), meanXpos + 4, MARGIN + 12)
  }

  private def drawMedianLine(g2: Graphics2D, width: Double): Unit = {
    val median = model.calcMedianPos
    val medianXpos = (MARGIN + width * median / model.numBars + barWidth / 2).toInt
    g2.drawLine(medianXpos, height - MARGIN, medianXpos, MARGIN)
    g2.drawString(AppContext.getLabel("MEDIAN"), medianXpos + 4, MARGIN + 28)
  }

  private def drawZeroLine(g2: Graphics2D, width: Double): Unit = {
    if (xFunction.getInverseValue(0) < 0) {
      g2.drawLine(zeroXPos, height - MARGIN, zeroXPos, MARGIN)
      g2.drawString("0", zeroXPos + 4, MARGIN + 38)
    }
  }

  private def calcZeroPos(): Int = {
    val zero = xFunction.getValue(0)
    (MARGIN + width * zero / model.numBars + barWidth / 2).toInt
  }

  /** Draw a single bar in the histogram */
  private def drawBar(g2: Graphics2D, scale: Double, xpos: Float, xIdx: Int, value: Int): Unit = {
    val h = scale * value
    val top = (height - h - MARGIN).toInt
    g2.setColor(BAR_COLOR)
    g2.fillRect(xpos.toInt, top, Math.max(1, barWidth).toInt, h.toInt)
    g2.setColor(BAR_BORDER_COLOR)
    if (model.numBars < maxNumLabels) { // if not too many bars add a nice border.
      g2.drawRect(xpos.toInt, top, barWidth.toInt, h.toInt)
    }
    drawLabelIfNeeded(g2, xpos, xIdx)
  }

  /** Draw the label or label and tick if needed for this bar.
    * Avoid drawing the x label if its close to 0 so it does not interfere with the 0 label.
    */
  private def drawLabelIfNeeded(g2: Graphics2D, xpos: Float, xIdx: Int): Unit = {
    val xValue = xFunction.getInverseValue(xIdx)
    val x = (xpos + barWidth / 2).toInt
    val metrics = g2.getFontMetrics
    var drawingLabel = false
    val labelSkip = (maxLabelWidth + 10) * model.numBars / width
    val closeToZero = Math.abs(xpos - zeroXPos) < maxLabelWidth / 2
    if (closeToZero) {
      g2.setFont(BOLD_FONT)
      g2.drawString(formatter.format(0), zeroXPos - 8, height - 5)
      g2.drawLine(zeroXPos, height - MARGIN + TICK_LENGTH + 4, zeroXPos, height - MARGIN - 2) // zero tick
      g2.setFont(FONT)
    }
    else if (model.numBars < maxNumLabels || xIdx % labelSkip == 0) {
      drawingLabel = true
      val label = formatter.format(xValue)
      val labelXPos = x - metrics.stringWidth(label) / 2
      g2.drawString(label, labelXPos, height - 5)
    }

    val skipD2 = Math.max(1, labelSkip / 2)
    val skipD5 = Math.max(1, labelSkip / 5)
    if (labelSkip % 2 == 0 && xIdx % skipD2 == 0)
      g2.drawLine(x, height - MARGIN + TICK_LENGTH + 1, x, height - MARGIN)
    else if (labelSkip % 5 == 0 && xIdx % skipD5 == 0)
      g2.drawLine(x, height - MARGIN + TICK_LENGTH - 2, x, height - MARGIN)
    if (drawingLabel)
      g2.drawLine(x, height - MARGIN + TICK_LENGTH + 4, x, height - MARGIN - 2)
  }

  private def clearBackground(g2: Graphics2D): Unit = {
    g2.setColor(BACKGROUND_COLOR)
    g2.fillRect(0, 0, width, height)
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy