package ocelot.desktop.ui.widget import ocelot.desktop.ColorScheme import ocelot.desktop.color.Color import ocelot.desktop.geometry.{Padding2D, Rect2D, Size2D} import ocelot.desktop.graphics.Graphics import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.event.handlers.{HoverHandler, MouseHandler} import ocelot.desktop.ui.event.{ClickEvent, MouseEvent} import ocelot.desktop.ui.widget.Plot.Axis import ocelot.desktop.ui.widget.contextmenu.ContextMenu import ocelot.desktop.util.DrawUtils abstract class Plot extends Widget { private var barsMin = new Array[Float](0) private var barsMax = new Array[Float](0) def xAxis: Axis def yAxis: Axis def points: (Array[Float], Array[Float]) def fill: Boolean = false def setupContextMenu(menu: ContextMenu): Unit = {} def onPanelLeftClick(): Unit = {} def onPanelRightClick(): Unit = { val menu = new ContextMenu setupContextMenu(menu) root.get.contextMenus.open(menu) } private val panel = new Widget with HoverHandler with MouseHandler { override protected def receiveClickEvents: Boolean = true eventHandlers += { case ClickEvent(MouseEvent.Button.Left, _) => onPanelLeftClick() case ClickEvent(MouseEvent.Button.Right, _) => onPanelRightClick() } override def minimumSize: Size2D = Size2D(50, 50) override def maximumSize: Size2D = Size2D.Inf override def draw(g: Graphics): Unit = { resample(bounds) val fillColor = ColorScheme("PlotFill") val gridColor = ColorScheme("PlotGrid") val lineColor = ColorScheme("PlotLine") DrawUtils.panel(g, bounds.x - 2, bounds.y - 2, bounds.w + 4, bounds.h + 4) for (tickX <- xAxis.ticks) { val x = xAxis.mapPos(tickX, bounds.w).floor g.line(bounds.x + x, bounds.y, bounds.x + x, bounds.y + bounds.h, 1, gridColor) } for (tickY <- yAxis.ticks) { val y = yAxis.mapPos(tickY, bounds.h).floor + 0.5f g.line(bounds.x, bounds.y + y, bounds.x + bounds.w, bounds.y + y, 1, gridColor) } if (fill) { for ((max, x) <- barsMax.zipWithIndex) { g.rect(bounds.x + x, bounds.y + max, 1, bounds.h - max, fillColor) } } for (((min, max), x) <- barsMin.zip(barsMax).zipWithIndex) { g.rect(bounds.x + x, bounds.y + min, 1, (max - min).max(1), lineColor) } } } children :+= new PaddingBox( panel, Padding2D( left = 4, right = if (yAxis.ticks.isEmpty || !xAxis.tickLabels) 4 else yAxis.tickWidth * 7 + 6, top = 4, bottom = if (xAxis.ticks.isEmpty || !xAxis.tickLabels) 4 else 14, ), ) override def draw(g: Graphics): Unit = { val xAxis = this.xAxis val yAxis = this.yAxis val pBounds = panel.bounds panel.draw(g) g.setSmallFont() g.background = Color.Transparent g.foreground = ColorScheme("PlotText") if (xAxis.tickLabels) { for (tickX <- xAxis.ticks) { val x = xAxis.mapPos(tickX, pBounds.w).floor val text = xAxis.tickFormatter(tickX) g.text(pBounds.x + x - (text.length * 3.5f).floor, pBounds.y + pBounds.h + 4, text, shrink = 1) } } if (yAxis.tickLabels) { for (tickY <- yAxis.ticks) { val y = yAxis.mapPos(tickY, pBounds.h).floor + 2 val text = yAxis.tickFormatter(tickY) g.text(pBounds.x + pBounds.w + 4, pBounds.y + y - 4, text, shrink = 1) } } if (panel.mouseOver) { g.foreground = ColorScheme("PlotLine") val pos = UiHandler.mousePosition val x = xAxis.inversePos(pos.x - pBounds.x, pBounds.w) val y = yAxis.inversePos(pos.y - pBounds.y, pBounds.h) val text = f"${xAxis.hoverFormatter(x)}${xAxis.unit}; ${yAxis.hoverFormatter(y)}${yAxis.unit}" g.text(pBounds.x + 1, pBounds.y + 1, text, shrink = 1) } g.setNormalFont() } private var mappedX: Array[Float] = new Array(0) private var mappedY: Array[Float] = new Array(0) private def resample(pBounds: Rect2D): Unit = { val xAxis = this.xAxis val yAxis = this.yAxis val numBars = pBounds.w.toInt if (barsMin.length != numBars) { barsMin = new Array(numBars) barsMax = new Array(numBars) } val points = this.points val numPoints = points._1.length assert(points._1.length == points._2.length) if (mappedX.length != numPoints) { mappedX = new Array(numPoints) mappedY = new Array(numPoints) } var i = 0 while (i < numPoints) { mappedX(i) = xAxis.mapPos(points._1(i), pBounds.w) mappedY(i) = yAxis.mapPos(points._2(i), pBounds.h) i += 1 } var pointIdx = 0 var barIdx = 0 while (barIdx < numBars) { var min = Float.PositiveInfinity var max = Float.NegativeInfinity var count = 0 while (pointIdx < numPoints && mappedX(pointIdx) <= barIdx) { val y = mappedY(pointIdx) if (y < min) min = y if (y > max) max = y pointIdx += 1 count += 1 } val savedIdx = pointIdx while (pointIdx < numPoints && mappedX(pointIdx) - barIdx <= 0.5f) { val y = mappedY(pointIdx) if (y < min) min = y if (y > max) max = y pointIdx += 1 count += 1 } pointIdx = savedIdx if (count > 0) { barsMin(barIdx) = min barsMax(barIdx) = max } else { val ax = mappedX(pointIdx - 1) val ay = mappedY(pointIdx - 1) val bx = if (pointIdx < numPoints) mappedX(pointIdx) else ax val by = if (pointIdx < numPoints) mappedY(pointIdx) else ay val t = (bx - barIdx) / (bx - ax) val s = ay * t + by * (1 - t) barsMin(barIdx) = s barsMax(barIdx) = s } if (barIdx >= 1) { if (barsMax(barIdx - 1) < barsMin(barIdx)) barsMax(barIdx - 1) = barsMin(barIdx) if (barsMin(barIdx - 1) > barsMax(barIdx)) barsMax(barIdx) = barsMin(barIdx - 1) } barIdx += 1 } } } object Plot { case class Axis(min: Float, max: Float, ticks: List[Float] = List(), flip: Boolean = false, logOffset: Option[Float] = None, tickFormatter: Float => String = _.toString, tickLabels: Boolean = true, hoverFormatter: Float => String = _.toString, unit: String = "") { lazy val tickWidth: Int = if (ticks.isEmpty || !tickLabels) 0 else ticks.map(tickFormatter(_).length).max def mapPos(pos: Float, size: Float): Float = { var t = if (logOffset.isDefined) { val o = logOffset.get val logPos = math.log(pos + o).toFloat val logMin = math.log(min + o).toFloat val logMax = math.log(max + o).toFloat (logPos - logMin) / (logMax - logMin) } else { (pos - min) / (max - min) } if (flip) t = 1 - t if (t > 1) t = 1 if (t < 0) t = 0 t * size } def inversePos(pos: Float, size: Float): Float = { var t = pos / size if (flip) t = 1 - t if (logOffset.isDefined) { val o = logOffset.get val logMin = math.log(min + o).toFloat val logMax = math.log(max + o).toFloat val logPos = t * (logMax - logMin) + logMin (math.exp(logPos) - o).toFloat } else { t * (max - min) + min } } } }