diff --git a/xchart-demo/src/main/java/org/knowm/xchart/standalone/issues/TestForIssue679.java b/xchart-demo/src/main/java/org/knowm/xchart/standalone/issues/TestForIssue679.java
new file mode 100644
index 000000000..7503fe477
--- /dev/null
+++ b/xchart-demo/src/main/java/org/knowm/xchart/standalone/issues/TestForIssue679.java
@@ -0,0 +1,60 @@
+package org.knowm.xchart.standalone.issues;
+
+import org.knowm.xchart.HeatMapChart;
+import org.knowm.xchart.HeatMapChartBuilder;
+import org.knowm.xchart.SwingWrapper;
+
+/**
+ * Demonstrates issue #679 — custom tooltips via {@code setToolTipGenerator(...)}, available on
+ * every series type.
+ *
+ *
The generator receives a {@code ChartDataPoint} carrying the series name, the data point's
+ * index within the series data and the default label, and returns the tooltip text to display
+ * (multi-line via {@code System.lineSeparator()}). Returning {@code null} falls back to the default
+ * label. Here each heat map cell's tooltip shows extra information looked up from an application
+ * data structure by the data point index — exactly what the issue asked for.
+ */
+public class TestForIssue679 {
+
+ public static void main(String[] args) {
+
+ SwingWrapper wrapper = new SwingWrapper<>(getChart());
+ wrapper.displayChart();
+ // tooltips are an XChartPanel feature and are off by default
+ wrapper.getXChartPanel().setToolTipsEnabled(true);
+ }
+
+ /** Constructs and returns the chart without launching a window (headless-safe). */
+ public static HeatMapChart getChart() {
+
+ HeatMapChart chart =
+ new HeatMapChartBuilder()
+ .width(700)
+ .height(400)
+ .title("Issue #679 – Custom HeatMap tooltips (hover over a cell)")
+ .build();
+ chart.getStyler().setShowValue(true);
+
+ int[] xData = {0, 1, 2, 3};
+ int[] yData = {0, 1, 2};
+ // heatData[x][y] = cell value; the data point index runs x-major: index = x * yLength + y
+ int[][] heatData = new int[xData.length][yData.length];
+ // application-side extra info, indexed the same as the heat data
+ String[] extraInfo = new String[xData.length * yData.length];
+ for (int x : xData) {
+ for (int y : yData) {
+ heatData[x][y] = (x + 1) * (y + 1);
+ extraInfo[x * yData.length + y] = "sample count: " + (100 + 10 * x + y);
+ }
+ }
+
+ chart
+ .addSeries("heat", xData, yData, heatData)
+ .setToolTipGenerator(
+ dataPoint ->
+ dataPoint.getLabel()
+ + System.lineSeparator()
+ + extraInfo[dataPoint.getDataPointIndex()]);
+ return chart;
+ }
+}
diff --git a/xchart/src/main/java/org/knowm/xchart/BubbleSeries.java b/xchart/src/main/java/org/knowm/xchart/BubbleSeries.java
index 93e0f5133..53a4eaa06 100644
--- a/xchart/src/main/java/org/knowm/xchart/BubbleSeries.java
+++ b/xchart/src/main/java/org/knowm/xchart/BubbleSeries.java
@@ -40,6 +40,11 @@ public BubbleSeries setBubbleSeriesRenderStyle(BubbleSeriesRenderStyle bubbleSer
return this;
}
+ /**
+ * @deprecated use {@link #setToolTipGenerator(ToolTipGenerator)} instead; will be removed in
+ * 4.1.0
+ */
+ @Deprecated
public boolean isCustomToolTips() {
return customToolTips;
@@ -52,13 +57,22 @@ public boolean isCustomToolTips() {
*
* @param customToolTips true to show the per-data-point strings from {@link
* #setToolTips(String[])}; false to show the default formatted x/y axis values
+ * @deprecated use {@link #setToolTipGenerator(ToolTipGenerator)} instead, which works on every
+ * chart type and takes precedence over these strings when both are set; will be removed in
+ * 4.1.0
*/
+ @Deprecated
public BubbleSeries setCustomToolTips(boolean customToolTips) {
this.customToolTips = customToolTips;
return this;
}
+ /**
+ * @deprecated use {@link #setToolTipGenerator(ToolTipGenerator)} instead; will be removed in
+ * 4.1.0
+ */
+ @Deprecated
public String[] getToolTips() {
return toolTips;
@@ -71,7 +85,11 @@ public String[] getToolTips() {
*
* @param toolTips the tooltip strings, one per data point; a null entry (or a null array) falls
* back to the default formatted x/y axis values for that data point
+ * @deprecated use {@link #setToolTipGenerator(ToolTipGenerator)} instead, which works on every
+ * chart type and takes precedence over these strings when both are set; will be removed in
+ * 4.1.0
*/
+ @Deprecated
public BubbleSeries setToolTips(String[] toolTips) {
this.toolTips = toolTips;
diff --git a/xchart/src/main/java/org/knowm/xchart/RadarSeries.java b/xchart/src/main/java/org/knowm/xchart/RadarSeries.java
index 9efe1858b..72576227b 100644
--- a/xchart/src/main/java/org/knowm/xchart/RadarSeries.java
+++ b/xchart/src/main/java/org/knowm/xchart/RadarSeries.java
@@ -27,10 +27,10 @@ public class RadarSeries extends MarkerSeries {
private double[] values;
private String[] tooltipOverrides;
- // TODO refactor tooltips override
/**
* @param tooltipOverrides Adds custom tooltipOverrides for series. If tooltipOverrides is null,
- * they are automatically generated.
+ * they are automatically generated. Deprecated: pass null and use {@link
+ * #setToolTipGenerator(ToolTipGenerator)} instead.
*/
public RadarSeries(String name, double[] values, String[] tooltipOverrides) {
@@ -50,6 +50,11 @@ public RadarSeries setValues(double[] values) {
return this;
}
+ /**
+ * @deprecated use {@link #setToolTipGenerator(ToolTipGenerator)} instead; will be removed in
+ * 4.1.0
+ */
+ @Deprecated
public String[] getTooltipOverrides() {
return tooltipOverrides;
@@ -154,6 +159,12 @@ public LegendRenderType getLegendRenderType() {
return LegendRenderType.Line;
}
+ /**
+ * @deprecated use {@link #setToolTipGenerator(ToolTipGenerator)} instead, which works on every
+ * chart type and takes precedence over these strings when both are set; will be removed in
+ * 4.1.0
+ */
+ @Deprecated
public RadarSeries setTooltipOverrides(String[] tooltipOverrides) {
this.tooltipOverrides = tooltipOverrides;
diff --git a/xchart/src/main/java/org/knowm/xchart/ToolTipGenerator.java b/xchart/src/main/java/org/knowm/xchart/ToolTipGenerator.java
new file mode 100644
index 000000000..01fc6bd5e
--- /dev/null
+++ b/xchart/src/main/java/org/knowm/xchart/ToolTipGenerator.java
@@ -0,0 +1,33 @@
+package org.knowm.xchart;
+
+import org.knowm.xchart.internal.series.Series;
+
+/**
+ * Generates custom tooltip text for a series' data points, replacing the default label built from
+ * the formatted axis values. Set one on any series via {@link
+ * Series#setToolTipGenerator(ToolTipGenerator)}:
+ *
+ *
+ * chart.getHeatMapSeries()
+ * .setToolTipGenerator(
+ * dataPoint -> "extra info for cell #" + dataPoint.getDataPointIndex());
+ *
+ *
+ * The generator is invoked once per rendered data point each time the chart is painted. The
+ * {@link ChartDataPoint} argument carries the series name, the data point's index within the
+ * series' data (so raw values can be looked up in the data structures the series was built from),
+ * the default formatted label or x/y values, and the data point's screen geometry.
+ *
+ *
Multi-line tooltips are supported: separate lines with {@link System#lineSeparator()}.
+ */
+@FunctionalInterface
+public interface ToolTipGenerator {
+
+ /**
+ * Generate the tooltip text for one data point.
+ *
+ * @param dataPoint the rendered data point the tooltip belongs to
+ * @return the tooltip text, or {@code null} to fall back to the default label
+ */
+ String generateToolTip(ChartDataPoint dataPoint);
+}
diff --git a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/DataPointDispatcher.java b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/DataPointDispatcher.java
index aa098475d..1060c3bd8 100644
--- a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/DataPointDispatcher.java
+++ b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/DataPointDispatcher.java
@@ -3,7 +3,6 @@
import java.awt.Shape;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
-import java.awt.geom.Ellipse2D;
import java.util.ArrayList;
import java.util.List;
import org.knowm.xchart.ChartDataPoint;
@@ -17,9 +16,6 @@
*/
public class DataPointDispatcher extends MouseAdapter {
- // Matches the default marker hit shape built in ToolTips.ToolTip when no explicit shape is given.
- private static final double MARGIN = 5;
-
private final List listeners;
private final List dataPoints = new ArrayList<>();
private ChartDataPoint hovered = null;
@@ -35,20 +31,15 @@ public void setData(PlotInteractionData data) {
return;
}
for (PlotInteractionData.ToolTipData td : data.getToolTipDataList()) {
- Shape shape = td.shape != null ? td.shape : defaultMarkerShape(td.x, td.y);
+ Shape shape = td.shape != null ? td.shape : PlotInteractionData.defaultMarkerShape(td.x, td.y);
+ // listeners see the label that is actually displayed, custom or default
+ String label = td.getCustomLabel() != null ? td.getCustomLabel() : td.label;
dataPoints.add(
new ChartDataPoint(
- td.seriesName, td.dataPointIndex, td.label, td.xValue, td.yValue, td.x, td.y, shape));
+ td.seriesName, td.dataPointIndex, label, td.xValue, td.yValue, td.x, td.y, shape));
}
}
- private static Shape defaultMarkerShape(double x, double y) {
-
- double halfSize = MARGIN * 1.5;
- double markerSize = MARGIN * 3;
- return new Ellipse2D.Double(x - halfSize, y - halfSize, markerSize, markerSize);
- }
-
private ChartDataPoint hitTest(int x, int y) {
for (ChartDataPoint dataPoint : dataPoints) {
diff --git a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_Box.java b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_Box.java
index c0a60e943..05c904938 100644
--- a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_Box.java
+++ b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_Box.java
@@ -82,7 +82,8 @@ protected void doPaint(Graphics2D g) {
}
// data points
double[] yArr = series.getYData();
- for (double yOrig : yArr) {
+ for (int dataIndex = 0; dataIndex < yArr.length; dataIndex++) {
+ double yOrig = yArr[dataIndex];
double y;
if (boxPlotStyler.isYAxisLogarithmic()) {
@@ -121,13 +122,15 @@ protected void doPaint(Graphics2D g) {
g.draw(outPointLine2);
if (toolTipsEnabled) {
- interactionData.addToolTip(
- xOffset,
- yOffset,
- series.getName()
- + ":"
- + System.lineSeparator()
- + axesChart.getYAxisFormat().format(yOrig));
+ interactionData
+ .addToolTip(
+ xOffset,
+ yOffset,
+ series.getName()
+ + ":"
+ + System.lineSeparator()
+ + axesChart.getYAxisFormat().format(yOrig))
+ .withSeries(series, dataIndex);
}
} else if (chart.getStyler().getShowWithinAreaPoint()) {
@@ -136,22 +139,24 @@ protected void doPaint(Graphics2D g) {
series.getMarker().paint(g, xOffset, yOffset, boxPlotStyler.getMarkerSize());
if (toolTipsEnabled) {
- interactionData.addToolTip(
- xOffset,
- yOffset,
- series.getName()
- + ":"
- + System.lineSeparator()
- + axesChart.getYAxisFormat().format(yOrig));
+ interactionData
+ .addToolTip(
+ xOffset,
+ yOffset,
+ series.getName()
+ + ":"
+ + System.lineSeparator()
+ + axesChart.getYAxisFormat().format(yOrig))
+ .withSeries(series, dataIndex);
}
}
}
- drawBoxPlot(g, series.getName(), boxPlotData);
+ drawBoxPlot(g, series, boxPlotData);
}
}
- private void drawBoxPlot(Graphics2D g, String seriesName, BoxPlotData boxPlotData) {
+ private void drawBoxPlot(Graphics2D g, S series, BoxPlotData boxPlotData) {
// when all data values are the same yMin == yMax; offsets would be NaN, so skip rendering
if (yMax == yMin) {
@@ -251,28 +256,31 @@ private void drawBoxPlot(Graphics2D g, String seriesName, BoxPlotData boxPlotDat
area.add(new Area(rect.getBounds()));
if (interactionData != null) {
- interactionData.addToolTip(
- area,
- xOffset,
- yOffset,
- 10,
- seriesName
- + ":"
- + System.lineSeparator()
- + "upper: "
- + axesChart.getYAxisFormat().format(boxPlotData.upper)
- + System.lineSeparator()
- + "q3: "
- + axesChart.getYAxisFormat().format(boxPlotData.q3)
- + System.lineSeparator()
- + "median: "
- + axesChart.getYAxisFormat().format(boxPlotData.median)
- + System.lineSeparator()
- + "q1: "
- + axesChart.getYAxisFormat().format(boxPlotData.q1)
- + System.lineSeparator()
- + "lower: "
- + axesChart.getYAxisFormat().format(boxPlotData.lower));
+ interactionData
+ .addToolTip(
+ area,
+ xOffset,
+ yOffset,
+ 10,
+ series.getName()
+ + ":"
+ + System.lineSeparator()
+ + "upper: "
+ + axesChart.getYAxisFormat().format(boxPlotData.upper)
+ + System.lineSeparator()
+ + "q3: "
+ + axesChart.getYAxisFormat().format(boxPlotData.q3)
+ + System.lineSeparator()
+ + "median: "
+ + axesChart.getYAxisFormat().format(boxPlotData.median)
+ + System.lineSeparator()
+ + "q1: "
+ + axesChart.getYAxisFormat().format(boxPlotData.q1)
+ + System.lineSeparator()
+ + "lower: "
+ + axesChart.getYAxisFormat().format(boxPlotData.lower))
+ // the box aggregates the whole series, so there is no single data point index
+ .withSeries(series, -1);
}
}
}
diff --git a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_Bubble.java b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_Bubble.java
index cd0b2b1a4..cd9c3c56d 100644
--- a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_Bubble.java
+++ b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_Bubble.java
@@ -142,7 +142,7 @@ public void doPaint(Graphics2D g) {
&& customToolTips[i] != null) {
interactionData
.addToolTip(bubble, xOffset, yOffset, 0, customToolTips[i])
- .withSeries(series.getName(), i);
+ .withSeries(series, i);
} else {
interactionData
.addToolTip(
@@ -152,7 +152,7 @@ public void doPaint(Graphics2D g) {
0,
axesChart.getXAxisFormat().format(x),
axesChart.getYAxisFormat().format(yOrig))
- .withSeries(series.getName(), i);
+ .withSeries(series, i);
}
}
}
diff --git a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_Category.java b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_Category.java
index 5a6c10945..a0133f310 100644
--- a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_Category.java
+++ b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_Category.java
@@ -521,7 +521,7 @@ public void doPaint(Graphics2D g) {
barWidth,
axesChart.getXAxisFormat().format(nextCat),
axesChart.getYAxisFormat().format(yOrig))
- .withSeries(series.getName(), dataIndex);
+ .withSeries(series, dataIndex);
}
}
diff --git a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_Dial.java b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_Dial.java
index fce97c65c..6e23cf9b9 100644
--- a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_Dial.java
+++ b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_Dial.java
@@ -242,7 +242,8 @@ public void doPaint(Graphics2D g) {
label = df.format(value);
}
}
- interactionData.addToolTip(path, xOffset, yOffset + 10, 0, label);
+ // a dial series is a single value, so its only data point is index 0
+ interactionData.addToolTip(path, xOffset, yOffset + 10, 0, label).withSeries(series, 0);
}
path.moveTo(xCenter, yCenter);
diff --git a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_HeatMap.java b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_HeatMap.java
index b60510ca8..ad11d90ae 100644
--- a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_HeatMap.java
+++ b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_HeatMap.java
@@ -72,7 +72,8 @@ protected void doPaint(Graphics2D g) {
double yOffset = 0.0;
Rectangle2D rect = null;
Color heatMapValueColor = null;
- for (Number[] numbers : list) {
+ for (int dataIndex = 0; dataIndex < list.size(); dataIndex++) {
+ Number[] numbers = list.get(dataIndex);
if (numbers == null) {
continue;
}
@@ -102,18 +103,20 @@ protected void doPaint(Graphics2D g) {
}
if (interactionData != null) {
- interactionData.addToolTip(
- rect,
- rect.getCenterX(),
- rect.getCenterY() + heatMapStyler.getToolTipFont().getSize(),
- 0,
- series.getName()
- + ": "
- + axesChart.getXAxisFormat().format(xData.get(x))
- + ", "
- + axesChart.getYAxisFormat().format(yData.get(y))
- + ", "
- + df.format(numbers[2]));
+ interactionData
+ .addToolTip(
+ rect,
+ rect.getCenterX(),
+ rect.getCenterY() + heatMapStyler.getToolTipFont().getSize(),
+ 0,
+ series.getName()
+ + ": "
+ + axesChart.getXAxisFormat().format(xData.get(x))
+ + ", "
+ + axesChart.getYAxisFormat().format(yData.get(y))
+ + ", "
+ + df.format(numbers[2]))
+ .withSeries(series, dataIndex);
}
}
}
diff --git a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_HorizontalBar.java b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_HorizontalBar.java
index c725f275b..78792c525 100644
--- a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_HorizontalBar.java
+++ b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_HorizontalBar.java
@@ -193,7 +193,7 @@ public void doPaint(Graphics2D g) {
axesChart.getXAxisFormat().format(xOrig),
axesChart.getYAxisFormat().format(nextCat))
// categoryCounter was post-incremented above, so this bar's index is one less
- .withSeries(series.getName(), categoryCounter - 1);
+ .withSeries(series, categoryCounter - 1);
}
}
diff --git a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_OHLC.java b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_OHLC.java
index b2cae5078..8f68dae69 100644
--- a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_OHLC.java
+++ b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_OHLC.java
@@ -142,11 +142,13 @@ public void doPaint(Graphics2D g) {
// add tooltips
if (interactionData != null) {
- interactionData.addToolTip(
- xOffset,
- yOffset,
- axesChart.getXAxisFormat().format(x),
- axesChart.getYAxisFormat(series.getYAxisDecimalPattern()).format(yOrig));
+ interactionData
+ .addToolTip(
+ xOffset,
+ yOffset,
+ axesChart.getXAxisFormat().format(x),
+ axesChart.getYAxisFormat(series.getYAxisDecimalPattern()).format(yOrig))
+ .withSeries(series, i);
}
}
} else {
@@ -324,7 +326,9 @@ public void doPaint(Graphics2D g) {
sb.append(System.lineSeparator())
.append("high: ")
.append(axesChart.getYAxisFormat().format(highOrig));
- interactionData.addToolTip(toolTipArea, xOffset, highOffset, 0, sb.toString());
+ interactionData
+ .addToolTip(toolTipArea, xOffset, highOffset, 0, sb.toString())
+ .withSeries(series, i);
}
}
}
diff --git a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_Pie.java b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_Pie.java
index cef68e11f..a63bd5590 100644
--- a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_Pie.java
+++ b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_Pie.java
@@ -210,7 +210,10 @@ private void paintSlices(Graphics2D g, Rectangle2D pieBounds, double total, doub
- Math.sin(Math.toRadians(angle))
* (pieBounds.getHeight() / 2 * pieStyler.getLabelsDistance());
- interactionData.addToolTip(toolTipShape, xOffset, yOffset + 10, 0, toolTipLabel);
+ // a pie series is a single slice, so its only data point is index 0
+ interactionData
+ .addToolTip(toolTipShape, xOffset, yOffset + 10, 0, toolTipLabel)
+ .withSeries(series, 0);
}
// TOOLTIPS ////////////////////////////////////////////////////
diff --git a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_Radar.java b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_Radar.java
index 6c401dc21..551bc0757 100644
--- a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_Radar.java
+++ b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_Radar.java
@@ -249,7 +249,7 @@ else if (styler.getRadarRenderStyle() == RadarStyler.RadarRenderStyle.Polygon) {
String ystr = decimalFormat.format(value);
label = series.getName() + " (" + radiiLabels[i] + ": " + ystr + ")";
}
- interactionData.addToolTip(xOffset, yOffset, label);
+ interactionData.addToolTip(xOffset, yOffset, label).withSeries(series, i);
}
}
path.closePath();
diff --git a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_XY.java b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_XY.java
index d53a5b164..91c77159b 100644
--- a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_XY.java
+++ b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_XY.java
@@ -277,7 +277,7 @@ public void doPaint(Graphics2D g) {
yOffset,
axesChart.getXAxisFormat().format(x),
axesChart.getYAxisFormat(series.getYAxisDecimalPattern()).format(yOrig))
- .withSeries(series.getName(), i);
+ .withSeries(series, i);
}
if (interactionData != null) {
diff --git a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotInteractionData.java b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotInteractionData.java
index 67c982958..a863354eb 100644
--- a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotInteractionData.java
+++ b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotInteractionData.java
@@ -1,13 +1,28 @@
package org.knowm.xchart.internal.chartpart;
import java.awt.Shape;
+import java.awt.geom.Ellipse2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import org.knowm.xchart.ChartDataPoint;
+import org.knowm.xchart.ToolTipGenerator;
+import org.knowm.xchart.internal.series.Series;
class PlotInteractionData {
+ // matches the marker highlight size used by ToolTips.ToolTip
+ private static final double DEFAULT_MARKER_MARGIN = 5;
+
+ /** The default hit shape used when a tooltip is added without an explicit shape. */
+ static Shape defaultMarkerShape(double x, double y) {
+
+ double halfSize = DEFAULT_MARKER_MARGIN * 1.5;
+ double markerSize = DEFAULT_MARKER_MARGIN * 3;
+ return new Ellipse2D.Double(x - halfSize, y - halfSize, markerSize, markerSize);
+ }
+
Rectangle2D plotBounds;
Rectangle2D getPlotBounds() {
@@ -71,6 +86,7 @@ static final class ToolTipData {
final String label; // non-null for single-label case
String seriesName; // series identity, null if the chart type doesn't report it
int dataPointIndex = -1; // index within the series, -1 if not reported
+ String customLabel; // from the series' ToolTipGenerator, null for the default label
ToolTipData(
Shape shape,
@@ -95,6 +111,29 @@ ToolTipData withSeries(String seriesName, int dataPointIndex) {
this.dataPointIndex = dataPointIndex;
return this;
}
+
+ /**
+ * Attaches series identity to this tooltip and, if the series has a {@link ToolTipGenerator},
+ * applies it to replace the default label. A custom label always takes precedence over the
+ * default label the chart type built, including the deprecated per-point tooltip strings.
+ */
+ ToolTipData withSeries(Series series, int dataPointIndex) {
+
+ withSeries(series.getName(), dataPointIndex);
+ ToolTipGenerator generator = series.getToolTipGenerator();
+ if (generator != null) {
+ Shape hitShape = shape != null ? shape : defaultMarkerShape(x, y);
+ customLabel =
+ generator.generateToolTip(
+ new ChartDataPoint(seriesName, dataPointIndex, label, xValue, yValue, x, y, hitShape));
+ }
+ return this;
+ }
+
+ /** The label to display: the generator's custom label if one was produced, else null. */
+ String getCustomLabel() {
+ return customLabel;
+ }
}
static final class CursorData {
diff --git a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/ToolTips.java b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/ToolTips.java
index 6c98752da..2e9c8b759 100644
--- a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/ToolTips.java
+++ b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/ToolTips.java
@@ -120,10 +120,11 @@ public void paint(Graphics2D g) {
// TODO need this null check??
if (tooltip != null) { // dataPoint was created in mouse move, need to render it
- // TODO See OHLC04. The line series are rendering as multi-line. Can we just define the
- // tooltip during creation and if it's multiline, paint it
- // as multiline??
- if (styler instanceof BoxStyler || styler instanceof OHLCStyler) {
+ // multi-line labels (Box/OHLC defaults, or custom labels containing line separators) get
+ // the multi-line treatment; everything else is painted as a single line
+ if (styler instanceof BoxStyler
+ || styler instanceof OHLCStyler
+ || tooltip.label.contains(System.lineSeparator())) {
paintMultiLineToolTip(g);
} else {
paintToolTip(g, tooltip);
@@ -338,7 +339,14 @@ public void setData(PlotInteractionData data) {
}
plotBounds = data.getPlotBounds();
for (PlotInteractionData.ToolTipData td : data.getToolTipDataList()) {
- if (td.label != null) {
+ // a custom label from the series' ToolTipGenerator replaces whatever default was built
+ if (td.getCustomLabel() != null) {
+ if (td.shape != null) {
+ addData(td.shape, td.x, td.y, td.w, td.getCustomLabel());
+ } else {
+ addData(td.x, td.y, td.getCustomLabel());
+ }
+ } else if (td.label != null) {
if (td.shape != null) {
addData(td.shape, td.x, td.y, td.w, td.label);
} else {
diff --git a/xchart/src/main/java/org/knowm/xchart/internal/series/Series.java b/xchart/src/main/java/org/knowm/xchart/internal/series/Series.java
index 91c21696c..ace052012 100644
--- a/xchart/src/main/java/org/knowm/xchart/internal/series/Series.java
+++ b/xchart/src/main/java/org/knowm/xchart/internal/series/Series.java
@@ -1,6 +1,7 @@
package org.knowm.xchart.internal.series;
import java.awt.*;
+import org.knowm.xchart.ToolTipGenerator;
import org.knowm.xchart.internal.chartpart.RenderableSeries.LegendRenderType;
/** A Series to be plotted on a Chart */
@@ -13,6 +14,9 @@ public abstract class Series {
private boolean showInLegend = true;
private boolean isEnabled = true;
+ /** generates custom tooltip text for this series' data points; null means default labels */
+ private ToolTipGenerator toolTipGenerator;
+
// TODO there is not always a y-axis group (pie chart for example) move this to an axis series
// tyoe??
private int yAxisGroup = 0;
@@ -112,6 +116,25 @@ public Series setYAxisDecimalPattern(String yAxisDecimalPattern) {
return this;
}
+ public ToolTipGenerator getToolTipGenerator() {
+
+ return toolTipGenerator;
+ }
+
+ /**
+ * Set a generator that produces custom tooltip text for this series' data points, replacing the
+ * default label built from the formatted axis values. The generator is called once per rendered
+ * data point on every repaint; returning {@code null} for a data point falls back to the default
+ * label. Requires tooltips to be enabled on the chart's styler.
+ *
+ * @param toolTipGenerator the generator, or {@code null} to restore the default labels
+ */
+ public Series setToolTipGenerator(ToolTipGenerator toolTipGenerator) {
+
+ this.toolTipGenerator = toolTipGenerator;
+ return this;
+ }
+
public enum DataType {
Number,
Date,
diff --git a/xchart/src/test/java/org/knowm/xchart/internal/chartpart/ToolTipGeneratorTest.java b/xchart/src/test/java/org/knowm/xchart/internal/chartpart/ToolTipGeneratorTest.java
new file mode 100644
index 000000000..5812199db
--- /dev/null
+++ b/xchart/src/test/java/org/knowm/xchart/internal/chartpart/ToolTipGeneratorTest.java
@@ -0,0 +1,190 @@
+package org.knowm.xchart.internal.chartpart;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.knowm.xchart.BubbleChart;
+import org.knowm.xchart.BubbleChartBuilder;
+import org.knowm.xchart.BubbleSeries;
+import org.knowm.xchart.ChartDataPoint;
+import org.knowm.xchart.HeatMapChart;
+import org.knowm.xchart.HeatMapChartBuilder;
+import org.knowm.xchart.XYChart;
+import org.knowm.xchart.XYChartBuilder;
+
+// https://github.com/knowm/XChart/issues/679
+// A ToolTipGenerator set on any series replaces the default tooltip labels with custom text built
+// from the ChartDataPoint (series name + data point index + default labels).
+//
+// Exercises the interaction data directly (no XChartPanel / Swing display) so it runs headless on
+// CI.
+class ToolTipGeneratorTest {
+
+ /** Accesses the package-private interaction data through a same-package Chart reference. */
+ private static PlotInteractionData interactionData(Chart, ?> chart) {
+
+ return chart.getInteractionData();
+ }
+
+ /** Paints the chart into an off-screen image so the interaction data gets collected. */
+ private static void render(Chart, ?> chart) {
+
+ BufferedImage image =
+ new BufferedImage(chart.getWidth(), chart.getHeight(), BufferedImage.TYPE_INT_ARGB);
+ Graphics2D g = image.createGraphics();
+ chart.paint(g, chart.getWidth(), chart.getHeight());
+ g.dispose();
+ }
+
+ @Test
+ void generatorReplacesDefaultLabelsOnXYChart() {
+
+ XYChart chart = new XYChartBuilder().width(800).height(600).build();
+ chart
+ .addSeries("s", new double[] {1, 2, 3}, new double[] {10, 20, 30})
+ .setToolTipGenerator(dataPoint -> "custom #" + dataPoint.getDataPointIndex());
+ chart.enableInteractionData();
+
+ render(chart);
+
+ List toolTips =
+ interactionData(chart).getToolTipDataList();
+ assertThat(toolTips).hasSize(3);
+ for (int i = 0; i < 3; i++) {
+ assertThat(toolTips.get(i).getCustomLabel()).isEqualTo("custom #" + i);
+ }
+ }
+
+ @Test
+ void generatorReceivesSeriesIdentityAndDefaultLabels() {
+
+ List seen = new ArrayList<>();
+
+ XYChart chart = new XYChartBuilder().width(800).height(600).build();
+ chart
+ .addSeries("mySeries", new double[] {1, 2}, new double[] {10, 20})
+ .setToolTipGenerator(
+ dataPoint -> {
+ seen.add(dataPoint);
+ return null; // fall back to the default label
+ });
+ chart.enableInteractionData();
+
+ render(chart);
+
+ assertThat(seen).hasSize(2);
+ assertThat(seen.get(0).getSeriesName()).isEqualTo("mySeries");
+ assertThat(seen.get(0).getDataPointIndex()).isZero();
+ assertThat(seen.get(1).getDataPointIndex()).isEqualTo(1);
+ // XY tooltips are x/y pairs, so the pair fields carry the default formatted values
+ assertThat(seen.get(1).getXValue()).isNotNull();
+ assertThat(seen.get(1).getYValue()).isNotNull();
+
+ // generator returned null, so the default labels stay in effect
+ for (PlotInteractionData.ToolTipData td : interactionData(chart).getToolTipDataList()) {
+ assertThat(td.getCustomLabel()).isNull();
+ }
+ }
+
+ // the original issue #679 request: extra per-cell information on a heat map
+ @Test
+ void generatorWorksOnHeatMapCells() {
+
+ HeatMapChart chart = new HeatMapChartBuilder().width(800).height(600).build();
+ List xData = java.util.Arrays.asList(0, 1);
+ List yData = java.util.Arrays.asList(0, 1);
+ List heatData = new ArrayList<>();
+ heatData.add(new Number[] {0, 0, 5});
+ heatData.add(new Number[] {0, 1, 6});
+ heatData.add(new Number[] {1, 0, 7});
+ heatData.add(new Number[] {1, 1, 8});
+ chart
+ .addSeries("heat", xData, yData, heatData)
+ .setToolTipGenerator(
+ dataPoint ->
+ "cell "
+ + dataPoint.getDataPointIndex()
+ + " extra info"
+ + System.lineSeparator()
+ + "default: "
+ + dataPoint.getLabel());
+ chart.enableInteractionData();
+
+ render(chart);
+
+ List toolTips =
+ interactionData(chart).getToolTipDataList();
+ assertThat(toolTips).hasSize(4);
+ for (int i = 0; i < 4; i++) {
+ assertThat(toolTips.get(i).seriesName).isEqualTo("heat");
+ assertThat(toolTips.get(i).getCustomLabel())
+ .startsWith("cell " + i + " extra info")
+ .contains("default: heat: ");
+ }
+ }
+
+ @Test
+ void generatorTakesPrecedenceOverDeprecatedPerPointStrings() {
+
+ BubbleChart chart = new BubbleChartBuilder().width(800).height(600).build();
+ BubbleSeries series =
+ chart.addSeries("b", new double[] {1, 2}, new double[] {10, 20}, new double[] {5, 5});
+ series.setCustomToolTips(true).setToolTips(new String[] {"old 0", "old 1"});
+ series.setToolTipGenerator(dataPoint -> "new " + dataPoint.getDataPointIndex());
+ chart.enableInteractionData();
+
+ render(chart);
+
+ List toolTips =
+ interactionData(chart).getToolTipDataList();
+ assertThat(toolTips).hasSize(2);
+ for (int i = 0; i < 2; i++) {
+ // the deprecated string became the default label, the generator's label wins
+ assertThat(toolTips.get(i).label).isEqualTo("old " + i);
+ assertThat(toolTips.get(i).getCustomLabel()).isEqualTo("new " + i);
+ }
+ }
+
+ @Test
+ void deprecatedPerPointStringsStillWorkWithoutGenerator() {
+
+ BubbleChart chart = new BubbleChartBuilder().width(800).height(600).build();
+ chart
+ .addSeries("b", new double[] {1, 2}, new double[] {10, 20}, new double[] {5, 5})
+ .setCustomToolTips(true)
+ .setToolTips(new String[] {"old 0", "old 1"});
+ chart.enableInteractionData();
+
+ render(chart);
+
+ List toolTips =
+ interactionData(chart).getToolTipDataList();
+ assertThat(toolTips).hasSize(2);
+ for (int i = 0; i < 2; i++) {
+ assertThat(toolTips.get(i).label).isEqualTo("old " + i);
+ assertThat(toolTips.get(i).getCustomLabel()).isNull();
+ }
+ }
+
+ @Test
+ void dispatcherReportsTheDisplayedCustomLabel() {
+
+ XYChart chart = new XYChartBuilder().width(800).height(600).build();
+ chart
+ .addSeries("s", new double[] {1}, new double[] {10})
+ .setToolTipGenerator(dataPoint -> "displayed");
+ chart.enableInteractionData();
+
+ render(chart);
+
+ DataPointDispatcher dispatcher = new DataPointDispatcher(new ArrayList<>());
+ dispatcher.setData(interactionData(chart));
+
+ PlotInteractionData.ToolTipData td = interactionData(chart).getToolTipDataList().get(0);
+ assertThat(dispatcher.isOverDataPoint((int) td.x, (int) td.y)).isTrue();
+ }
+}