diff --git a/xchart-demo/src/main/java/org/knowm/xchart/demo/charts/bubble/BubbleChart02.java b/xchart-demo/src/main/java/org/knowm/xchart/demo/charts/bubble/BubbleChart02.java
new file mode 100644
index 000000000..af6a63f7a
--- /dev/null
+++ b/xchart-demo/src/main/java/org/knowm/xchart/demo/charts/bubble/BubbleChart02.java
@@ -0,0 +1,80 @@
+package org.knowm.xchart.demo.charts.bubble;
+
+import java.text.DecimalFormat;
+import org.knowm.xchart.BubbleChart;
+import org.knowm.xchart.BubbleChartBuilder;
+import org.knowm.xchart.BubbleSeries;
+import org.knowm.xchart.SwingWrapper;
+import org.knowm.xchart.XChartPanel;
+import org.knowm.xchart.demo.charts.ExampleChart;
+import org.knowm.xchart.style.Styler;
+
+/**
+ * Bubble Chart with Custom Tooltips
+ *
+ *
Demonstrates the following:
+ *
+ *
+ * - Custom, per-data-point tooltip strings via {@link BubbleSeries#setToolTips(String[])}
+ *
- Tooltips always visible
+ */
+public class BubbleChart02 implements ExampleChart {
+
+ public static void main(String[] args) {
+
+ ExampleChart exampleChart = new BubbleChart02();
+ BubbleChart chart = exampleChart.getChart();
+ SwingWrapper wrapper = new SwingWrapper<>(chart);
+ wrapper.displayChart();
+ wrapper.getXChartPanel().setToolTipsEnabled(true);
+ }
+
+ @Override
+ public BubbleChart getChart() {
+
+ // Create Chart
+ BubbleChart chart =
+ new BubbleChartBuilder()
+ .width(800)
+ .height(600)
+ .title(getClass().getSimpleName())
+ .xAxisTitle("Volume")
+ .yAxisTitle("Rate")
+ .build();
+ chart.getStyler().setLegendPosition(Styler.LegendPosition.InsideSW);
+ chart.getStyler().setLegendLayout(Styler.LegendLayout.Horizontal);
+ chart.getStyler().setYAxisDecimalPattern("#%");
+ chart.getStyler().setToolTipsAlwaysVisible(true);
+
+ // Series: volume (x), rate (y), number of hits (bubble size)
+ double[] volume = new double[] {1298, 843, 2755, 1520};
+ double[] rate = new double[] {0.0215, 0.041, 0.017, 0.033};
+ double[] hits = new double[] {279, 346, 468, 502};
+
+ BubbleSeries series = chart.addSeries("conversion", volume, rate, hits);
+
+ // Build a custom tooltip string per data point combining all three values, e.g. "2% (279/1298)"
+ DecimalFormat percentFormat = new DecimalFormat("#%");
+ String[] toolTips = new String[volume.length];
+ for (int i = 0; i < volume.length; i++) {
+ toolTips[i] =
+ percentFormat.format(rate[i]) + " (" + (int) hits[i] + "/" + (int) volume[i] + ")";
+ }
+ series.setCustomToolTips(true);
+ series.setToolTips(toolTips);
+
+ return chart;
+ }
+
+ @Override
+ public void customizePanel(XChartPanel panel) {
+
+ panel.setToolTipsEnabled(true);
+ }
+
+ @Override
+ public String getExampleChartName() {
+
+ return getClass().getSimpleName() + " - Bubble Chart with Custom Tooltips";
+ }
+}
diff --git a/xchart-demo/src/main/java/org/knowm/xchart/standalone/issues/TestForIssue545.java b/xchart-demo/src/main/java/org/knowm/xchart/standalone/issues/TestForIssue545.java
index 1e1d49c2a..e12b200a7 100644
--- a/xchart-demo/src/main/java/org/knowm/xchart/standalone/issues/TestForIssue545.java
+++ b/xchart-demo/src/main/java/org/knowm/xchart/standalone/issues/TestForIssue545.java
@@ -1,9 +1,12 @@
package org.knowm.xchart.standalone.issues;
import java.awt.Font;
+import java.text.DecimalFormat;
import java.text.ParseException;
+
import org.knowm.xchart.BubbleChart;
import org.knowm.xchart.BubbleChartBuilder;
+import org.knowm.xchart.BubbleSeries;
import org.knowm.xchart.SwingWrapper;
import org.knowm.xchart.style.BubbleStyler;
import org.knowm.xchart.style.Styler;
@@ -30,14 +33,18 @@ public static BubbleChart getBubbleChart() {
.yAxisTitle("Rate")
.build();
setBubbleStyler(chart);
- chart.addSeries(
- "serieName", new double[] {data[0]}, new double[] {data[1]}, new double[] {data[2]});
- // bubbleSeries.setCustomToolTips(true);
- // String tooltip =
- // new DecimalFormat("#%").format(data[1]) + " (" + ((int) data[2]) + "/" + ((int)
- // data[0]) + ")";
- //
- // bubbleSeries.setToolTips(new String[] {tooltip});
+ BubbleSeries bubbleSeries =
+ chart.addSeries(
+ "seriesName", new double[] {data[0]}, new double[] {data[1]}, new double[] {data[2]});
+ String tooltip =
+ new DecimalFormat("#%").format(data[1])
+ + " ("
+ + ((int) data[2])
+ + "/"
+ + ((int) data[0])
+ + ")";
+ bubbleSeries.setCustomToolTips(true);
+ bubbleSeries.setToolTips(new String[] {tooltip});
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 853ccb96e..93e0f5133 100644
--- a/xchart/src/main/java/org/knowm/xchart/BubbleSeries.java
+++ b/xchart/src/main/java/org/knowm/xchart/BubbleSeries.java
@@ -10,6 +10,12 @@ public class BubbleSeries extends NoMarkersSeries {
private Optional bubbleSeriesRenderStyle = Optional.empty();
+ /** whether to use the custom, per-data-point tooltip strings instead of the x/y axis values */
+ private boolean customToolTips;
+
+ /** the custom tooltip strings, one per data point, indexed the same as the x/y/size data */
+ private String[] toolTips;
+
/**
* Constructor
*
@@ -34,6 +40,44 @@ public BubbleSeries setBubbleSeriesRenderStyle(BubbleSeriesRenderStyle bubbleSer
return this;
}
+ public boolean isCustomToolTips() {
+
+ return customToolTips;
+ }
+
+ /**
+ * Set whether to show the custom, per-data-point tooltip strings set via {@link
+ * #setToolTips(String[])} instead of the default formatted x/y axis values. Requires tooltips to
+ * be enabled on the styler.
+ *
+ * @param customToolTips true to show the per-data-point strings from {@link
+ * #setToolTips(String[])}; false to show the default formatted x/y axis values
+ */
+ public BubbleSeries setCustomToolTips(boolean customToolTips) {
+
+ this.customToolTips = customToolTips;
+ return this;
+ }
+
+ public String[] getToolTips() {
+
+ return toolTips;
+ }
+
+ /**
+ * Set the custom tooltip strings, one per data point, indexed the same as the x/y/bubble-size
+ * data. A null entry (or a null array) falls back to the default formatted x/y axis values for
+ * that data point. Also requires {@link #setCustomToolTips(boolean)} to be set to true.
+ *
+ * @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
+ */
+ public BubbleSeries setToolTips(String[] toolTips) {
+
+ this.toolTips = toolTips;
+ return this;
+ }
+
@Override
public LegendRenderType getLegendRenderType() {
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 7487b1451..c33d17c75 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
@@ -134,13 +134,22 @@ public void doPaint(Graphics2D g) {
// add tooltips
if (interactionData != null) {
- interactionData.addToolTip(
- bubble,
- xOffset,
- yOffset,
- 0,
- axesChart.getXAxisFormat().format(x),
- axesChart.getYAxisFormat().format(yOrig));
+
+ String[] customToolTips = series.getToolTips();
+ if (series.isCustomToolTips()
+ && customToolTips != null
+ && i < customToolTips.length
+ && customToolTips[i] != null) {
+ interactionData.addToolTip(bubble, xOffset, yOffset, 0, customToolTips[i]);
+ } else {
+ interactionData.addToolTip(
+ bubble,
+ xOffset,
+ yOffset,
+ 0,
+ axesChart.getXAxisFormat().format(x),
+ axesChart.getYAxisFormat().format(yOrig));
+ }
}
}
}
diff --git a/xchart/src/test/java/org/knowm/xchart/BubbleChartTest.java b/xchart/src/test/java/org/knowm/xchart/BubbleChartTest.java
new file mode 100644
index 000000000..245bc1985
--- /dev/null
+++ b/xchart/src/test/java/org/knowm/xchart/BubbleChartTest.java
@@ -0,0 +1,73 @@
+package org.knowm.xchart;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+import java.awt.image.BufferedImage;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class BubbleChartTest {
+
+ private BubbleChart chart;
+
+ @BeforeEach
+ void setUp() {
+
+ chart = new BubbleChartBuilder().title("test").xAxisTitle("X").yAxisTitle("Y").build();
+ }
+
+ // https://github.com/knowm/XChart/issues/545
+ @Test
+ void customToolTipsDefaultOff() {
+
+ BubbleSeries series =
+ chart.addSeries("s", new double[] {1, 2}, new double[] {3, 4}, new double[] {5, 6});
+
+ assertThat(series.isCustomToolTips()).isFalse();
+ assertThat(series.getToolTips()).isNull();
+ }
+
+ // https://github.com/knowm/XChart/issues/545
+ @Test
+ void customToolTipsRoundTrip() {
+
+ BubbleSeries series =
+ chart.addSeries("s", new double[] {1, 2}, new double[] {3, 4}, new double[] {5, 6});
+
+ String[] toolTips = {"2% (279/1298)", "4% (346/843)"};
+ series.setCustomToolTips(true);
+ series.setToolTips(toolTips);
+
+ assertThat(series.isCustomToolTips()).isTrue();
+ assertThat(series.getToolTips()).containsExactly("2% (279/1298)", "4% (346/843)");
+ }
+
+ // https://github.com/knowm/XChart/issues/545
+ @Test
+ void renderWithCustomToolTipsDoesNotThrow() {
+
+ BubbleSeries series =
+ chart.addSeries("s", new double[] {1, 2}, new double[] {3, 4}, new double[] {5, 6});
+ series.setCustomToolTips(true);
+ series.setToolTips(new String[] {"2% (279/1298)", "4% (346/843)"});
+ chart.getStyler().setToolTipsAlwaysVisible(true);
+
+ BufferedImage image = assertDoesNotThrow(() -> BitmapEncoder.getBufferedImage(chart));
+ assertThat(image).isNotNull();
+ }
+
+ // https://github.com/knowm/XChart/issues/545
+ @Test
+ void renderWithPartialCustomToolTipsFallsBackWithoutThrowing() {
+
+ // fewer tooltip entries than data points, plus a null entry -> falls back to axis values
+ BubbleSeries series =
+ chart.addSeries("s", new double[] {1, 2, 3}, new double[] {3, 4, 5}, new double[] {5, 6, 7});
+ series.setCustomToolTips(true);
+ series.setToolTips(new String[] {null});
+ chart.getStyler().setToolTipsAlwaysVisible(true);
+
+ assertDoesNotThrow(() -> BitmapEncoder.getBufferedImage(chart));
+ }
+}