JavaFX Use diagram Legend to toggle show / hide Possible series?
Can a chart legend be used to toggle show / hide series?
I got LineChart
with the legend and too much Series
for you to not read the information well. I was wondering if there is a way to use a legend to switch the series to show / hide?
Most of my names are Series
quite long, and it looks very strange if they are written twice in the legend, so you know which color wich belongs to Series
and the second time besides CheckBox
to switch them.
Edit1: Maybe I was unclear, even if there is no built-in function for this, I could use some input for what workaroud would look like because I can't think of anything.
source to share
This is how I solved it - I don't know of a simpler built-in solution
LineChart<Number, Number> chart;
for (Node n : chart.getChildrenUnmodifiable()) {
if (n instanceof Legend) {
Legend l = (Legend) n;
for (Legend.LegendItem li : l.getItems()) {
for (XYChart.Series<Number, Number> s : chart.getData()) {
if (s.getName().equals(li.getText())) {
li.getSymbol().setCursor(Cursor.HAND); // Hint user that legend symbol is clickable
li.getSymbol().setOnMouseClicked(me -> {
if (me.getButton() == MouseButton.PRIMARY) {
s.getNode().setVisible(!s.getNode().isVisible()); // Toggle visibility of line
for (XYChart.Data<Number, Number> d : s.getData()) {
if (d.getNode() != null) {
d.getNode().setVisible(s.getNode().isVisible()); // Toggle visibility of every node in the series
}
}
}
});
break;
}
}
}
}
}
You need to run this code once on your diagram ( LineChart
in this example, but you can probably adapt it to any other diagram). I find the child Legend
and then loop through all of its elements. I am mapping the legend item to the correct series based on the name - in my experience they always match and I couldn't find a better way to match them. Then it's just a matter of adding the correct event handler to that particular legend item.
source to share
For reference, a similar approach works with JFreeChart
in JavaFX as shown here . Adapted from this example, the option below adds ChartMouseListenerFX
to ChartViewer
. Click on the series or its legend element to make the series invisible; click elsewhere to restore it.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.entity.ChartEntity;
import org.jfree.chart.entity.LegendItemEntity;
import org.jfree.chart.entity.XYItemEntity;
import org.jfree.chart.fx.ChartViewer;
import org.jfree.chart.fx.interaction.ChartMouseEventFX;
import org.jfree.chart.fx.interaction.ChartMouseListenerFX;
import org.jfree.chart.labels.StandardXYToolTipGenerator;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
/**
* @see https://stackoverflow.com/a/44967809/230513
* @see /questions/982500/choose-the-series-of-data-that-you-want-to-display/3395077#3395077
*/
public class VisibleTest extends Application {
@Override
public void start(Stage stage) {
XYSeriesCollection dataset = new XYSeriesCollection();
for (int i = 0; i < 3; i++) {
XYSeries series = new XYSeries("value" + i);
for (double t = 0; t < 2 * Math.PI; t += 0.5) {
series.add(t, Math.sin(t) + i);
}
dataset.addSeries(series);
}
NumberAxis xAxis = new NumberAxis("domain");
NumberAxis yAxis = new NumberAxis("range");
XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer(true, true);
renderer.setBaseToolTipGenerator(new StandardXYToolTipGenerator());
XYPlot plot = new XYPlot(dataset, xAxis, yAxis, renderer);
JFreeChart chart = new JFreeChart("Test", plot);
ChartViewer viewer = new ChartViewer(chart);
viewer.addChartMouseListener(new ChartMouseListenerFX() {
@Override
public void chartMouseClicked(ChartMouseEventFX e) {
ChartEntity ce = e.getEntity();
if (ce instanceof XYItemEntity) {
XYItemEntity item = (XYItemEntity) ce;
renderer.setSeriesVisible(item.getSeriesIndex(), false);
} else if (ce instanceof LegendItemEntity) {
LegendItemEntity item = (LegendItemEntity) ce;
Comparable key = item.getSeriesKey();
renderer.setSeriesVisible(dataset.getSeriesIndex(key), false);
} else {
for (int i = 0; i < dataset.getSeriesCount(); i++) {
renderer.setSeriesVisible(i, true);
}
}
}
@Override
public void chartMouseMoved(ChartMouseEventFX e) {}
});
stage.setScene(new Scene(viewer));
stage.setTitle("JFreeChartFX");
stage.setWidth(640);
stage.setHeight(480);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
source to share
Thanks for @sillyfly's answer. I was able to post this to Kotlin. It turns out cleanly and concisely with the notation forEach
and filter
.
(Kotlin folk, please let me know any improvements, thanks).
lineChart.childrenUnmodifiable.forEach { if (it is Legend) {
it.items.forEach {
val li = it
lineChart.data.filter { it.name == li.text }.forEach {
li.symbol.cursor = Cursor.HAND
val s = it
li.symbol.setOnMouseClicked { if (it.button == MouseButton.PRIMARY) {
s.node.isVisible = !s.node.isVisible
s.data.forEach { it.node.isVisible = !it.node.isVisible }
}}
}
}
}
}
source to share