Skip to content

Commit

Permalink
feature: Show a tooltip for the chart
Browse files Browse the repository at this point in the history
  • Loading branch information
bric3 committed May 7, 2024
1 parent 2e230cd commit bbff974
Show file tree
Hide file tree
Showing 10 changed files with 205 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import java.awt.RenderingHints
import java.awt.geom.Rectangle2D
import java.beans.PropertyChangeListener
import java.beans.PropertyChangeSupport
import javax.swing.*

/**
* A chart that can be inlaid into a small space.
Expand Down Expand Up @@ -73,7 +74,7 @@ class Chart : RectangleContent {

/**
* The insets for the plot area (defaults to zero but can be modified to add space for
* annotations etc).
* annotations, etc.).
*/
var plotInsets = RectangleMargin(0.0, 0.0, 0.0, 0.0)
set(value) {
Expand All @@ -86,6 +87,26 @@ class Chart : RectangleContent {
propertyChangeSupport.firePropertyChange("plotInsets", oldPlotInsets, value)
}

fun createToolTipComponent(bounds: Rectangle2D, mousePosition: Point?): JComponent? {
val plotArea = plotInsets.shrink(bounds)
val constituents = chartSpecifications.mapNotNull {
val toolTipComponentContributor = configureRenderer(it.renderer) as? ToolTipComponentContributor
toolTipComponentContributor?.createToolTipComponent(it, plotArea, mousePosition)
}
constituents.ifEmpty { return null }

return JPanel().apply {
layout = BoxLayout(this, BoxLayout.Y_AXIS)

constituents.forEachIndexed { index, it ->
add(it)
if (index < constituents.size - 1) {
add(JSeparator())
}
}
}
}

/**
* Creates a new chart with the given specifications, dataset and renderer.
*
Expand Down Expand Up @@ -139,6 +160,7 @@ class Chart : RectangleContent {
is LineRendererDescriptor -> lineChartRenderer.apply {
linePaint = rendererSpec.lineColor ?: Color.BLACK
fillColors = rendererSpec.fillColors
tooltipFunction = rendererSpec.tooltipFunction
}

else -> error("Unsupported render spec")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ class ChartComponent(chart: Chart? = null) : JComponent() {
propertyChangeSupport.firePropertyChange("chart", oldChart, value)
}

val toolTipComponent: JComponent?
get() = chart?.createToolTipComponent(getBounds(rect), mousePosition)

/** A reusable rectangle to avoid creating work for the garbage collector. */
private val rect = Rectangle()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@
*/
package io.github.bric3.fireplace.charts

import io.github.bric3.fireplace.charts.XYDataset.XY

interface ChartDataset {
val label: String
val rangeOfX: Range<Long>
val rangeOfY: Range<Double>
val itemCount: Int
fun xAt(index: Int): Long
fun yAt(index: Int): Double?
fun xyAt(index: Int): XY<Long, Double>
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
*/
package io.github.bric3.fireplace.charts

import io.github.bric3.fireplace.charts.XYDataset.XY
import java.awt.Color
import javax.swing.*

/**
* Specifies the various properties of a chart (dataset, label, how it's rendered).
Expand All @@ -30,5 +32,6 @@ data class ChartSpecification(
data class LineRendererDescriptor(
val lineColor: Color? = null,
val fillColors: List<Color>? = null,
val tooltipFunction: (XY<Long, Double>, String) -> JComponent? = { _, _ -> null },
) : RendererDescriptor
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
package io.github.bric3.fireplace.charts

import java.awt.Color
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.concurrent.TimeUnit.MILLISECONDS


Expand All @@ -20,13 +23,32 @@ fun Color.withAlpha(alpha: Float) = Color(
(alpha * 255).toInt()
)

fun Color.withAlpha(alpha: Int): Color {
require(alpha in 0..255) { "Alpha value must be between 0 and 255" }
return Color(
red,
green,
blue,
alpha
)
}

fun presentableDuration(durationMs: Long): String {
val millis = durationMs % 1000L
val seconds = MILLISECONDS.toSeconds(durationMs) % 60L
val minutes = MILLISECONDS.toMinutes(durationMs) % 60L
val hours = MILLISECONDS.toHours(durationMs)
return String.format(
"%02d:%02d:%02d.%03d",
arrayOf(hours, minutes, seconds, millis)
hours, minutes, seconds, millis
)
}

private val timestampFormatter = DateTimeFormatter.ofPattern("LLL d, H:mm:ss")
fun presentableTime(epochMs: Long): String {
return timestampFormatter.format(Instant.ofEpochMilli(epochMs).atZone(ZoneId.systemDefault()))
}

fun presentablePercentage(value: Double): String {
return String.format("%.2f%%", value * 100)
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/
package io.github.bric3.fireplace.charts

import io.github.bric3.fireplace.charts.XYDataset.XY
import io.github.bric3.fireplace.core.ui.Colors
import io.github.bric3.fireplace.core.ui.LightDarkColor
import io.github.bric3.fireplace.ui.toolkit.UIUtil
Expand All @@ -22,6 +23,7 @@ import java.awt.Stroke
import java.awt.geom.Ellipse2D
import java.awt.geom.Path2D
import java.awt.geom.Rectangle2D
import javax.swing.*
import kotlin.math.abs


Expand All @@ -38,7 +40,7 @@ class LineChartRenderer(
BasicStroke.JOIN_ROUND
),
paint: Paint = LightDarkColor(Color.BLACK, Color.WHITE),
) : ChartRenderer {
) : ChartRenderer, ToolTipComponentContributor {

/**
* The gradient fill colors, either `null`, one color, or at most two colors.
Expand All @@ -58,6 +60,11 @@ class LineChartRenderer(
*/
var linePaint = paint

/**
* The tooltip function that will be called to create a tooltip component for a given XY value.
*/
var tooltipFunction: (XY<Long, Double>, String) -> JComponent? = { _, _ -> null }

/**
* A shape that will be drawn at the point of the last value in the series.
* Leave as null if you don't want anything drawn.
Expand Down Expand Up @@ -227,6 +234,40 @@ class LineChartRenderer(
}
}

override fun createToolTipComponent(
chart: ChartSpecification,
plotBounds: Rectangle2D,
mousePosition: Point?
): JComponent? {
val dataset = chart.dataset
val itemCount = dataset.itemCount
if (itemCount == 0) return null
val xRange = dataset.rangeOfX
var yRange = dataset.rangeOfY
if (includeZeroInYRange) {
yRange = yRange.include(0.0)
}

val hasContributor = mousePosition != null && mousePosition.x >= plotBounds.x && mousePosition.x <= plotBounds.maxX
if (!hasContributor) return null

var closestXY: XY<Long, Double>? = null
var closestItemX = Double.MAX_VALUE
for (i in 0 until itemCount) {
if (yRange.isZeroLength) continue
val xy = dataset.xyAt(i)
val xx = plotBounds.x + xRange.ratioFor(xy.x) * plotBounds.width
// discover the closest item to the mouse adjusted position
val distance = abs(xx - mousePosition!!.x)
if (distance < closestItemX) {
closestItemX = distance
closestXY = xy
}
}

return closestXY?.let { tooltipFunction(it, chart.label) }
}

companion object {
/**
* Creates a new [GradientPaint] instance based on the supplied colors and with coordinates
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Fireplace
*
* Copyright (c) 2021, Today - Brice Dutheil
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package io.github.bric3.fireplace.charts

import java.awt.Point
import java.awt.geom.Rectangle2D
import javax.swing.*

interface ToolTipComponentContributor {
fun createToolTipComponent(chart: ChartSpecification, plotArea: Rectangle2D, mousePosition: Point?): JComponent?
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ open class XYDataset(
override val itemCount: Int
get() = items.size

override fun xyAt(index: Int): XY<Long, Double> {
return items[index]
}

override fun xAt(index: Int): Long {
return items[index].x
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@ import io.github.bric3.fireplace.charts.ChartSpecification
import io.github.bric3.fireplace.charts.ChartSpecification.LineRendererDescriptor
import io.github.bric3.fireplace.charts.XYDataset.XY
import io.github.bric3.fireplace.charts.XYPercentageDataset
import io.github.bric3.fireplace.charts.presentablePercentage
import io.github.bric3.fireplace.charts.presentableTime
import io.github.bric3.fireplace.charts.withAlpha
import io.github.bric3.fireplace.jfr.support.JFRLoaderBinder
import io.github.bric3.fireplace.jfr.support.JfrAnalyzer
import io.github.bric3.fireplace.jfr.support.getMemberFromEvent
import io.github.bric3.fireplace.ui.CPU_BASE
import io.github.bric3.fireplace.ui.ThreadFlamegraphView
import io.github.bric3.fireplace.ui.ViewPanel.Priority
import io.github.bric3.fireplace.ui.toolkit.ColorIcon
import io.github.bric3.fireplace.ui.toolkit.FollowingTipService
import org.openjdk.jmc.common.IDisplayable
import org.openjdk.jmc.common.unit.UnitLookup
import org.openjdk.jmc.flightrecorder.JfrAttributes
Expand All @@ -41,6 +45,10 @@ class MethodCpuSample(jfrBinder: JFRLoaderBinder) : ThreadFlamegraphView(jfrBind
minimumSize = Dimension(99999, 84)
size = size.apply { height = 84 }
preferredSize = Dimension(99999, 84)

FollowingTipService.enableFor(this) { chart, _ ->
chart.toolTipComponent
}
}

jfrBinder.bindEvents(
Expand Down Expand Up @@ -93,6 +101,12 @@ class MethodCpuSample(jfrBinder: JFRLoaderBinder) : ThreadFlamegraphView(jfrBind

Chart(
buildList {
fun tooltipFunction(color: Color): (XY<Long, Double>, String) -> JComponent = { xy, label ->
JLabel("<html>$label: <strong>${presentablePercentage(xy.y)}</strong> at <strong>${presentableTime(xy.x)}</strong></html>").apply {
icon = ColorIcon(14, color)
}
}

if (cpuUserLoadValues.isNotEmpty()) {
add(
ChartSpecification(
Expand All @@ -101,6 +115,7 @@ class MethodCpuSample(jfrBinder: JFRLoaderBinder) : ThreadFlamegraphView(jfrBind
LineRendererDescriptor(
lineColor = Color.GREEN,
fillColors = listOf(Color.GREEN.withAlpha(0.4f), Color.GREEN.withAlpha(0.01f)),
tooltipFunction = tooltipFunction(Color.GREEN)
)
)
)
Expand All @@ -113,6 +128,7 @@ class MethodCpuSample(jfrBinder: JFRLoaderBinder) : ThreadFlamegraphView(jfrBind
LineRendererDescriptor(
lineColor = Color.RED,
fillColors = listOf(Color.RED.withAlpha(0.4f), Color.RED.withAlpha(0.01f)),
tooltipFunction = tooltipFunction(Color.RED)
)
)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Fireplace
*
* Copyright (c) 2021, Today - Brice Dutheil
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package io.github.bric3.fireplace.ui.toolkit

import io.github.bric3.fireplace.charts.withAlpha
import java.awt.Color
import java.awt.Component
import java.awt.Graphics
import java.awt.Graphics2D
import java.awt.RenderingHints
import javax.swing.*

open class ColorIcon(
private val width: Int,
private val height: Int,
private val colorWidth: Int,
private val colorHeight: Int,
private val color: Color,
private val isPaintBorder: Boolean,
private val arc: Int = 6
) : Icon {
constructor(size: Int, colorSize: Int, color: Color, border: Boolean) : this(
size,
size,
colorSize,
colorSize,
color,
border,
6
)

constructor(size: Int, color: Color, border: Boolean = false) : this(size, size, color, border)

override fun paintIcon(component: Component, g: Graphics, i: Int, j: Int) {
val g2d = g.create() as Graphics2D
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE)
g2d.color = color

val width = colorWidth
val height = colorHeight
val arc = arc
val x = i + (this.width - width) / 2
val y = j + (this.height - height) / 2

g2d.fillRoundRect(x, y, width, height, arc, arc)

if (isPaintBorder) {
g2d.color = Color(0, true).withAlpha(40) // TODO put in Colors
g2d.drawRoundRect(x, y, width, height, arc, arc)
}

g2d.dispose()
}

override fun getIconWidth(): Int {
return width
}

override fun getIconHeight(): Int {
return height
}
}

0 comments on commit bbff974

Please sign in to comment.