问题
I have four series on LineChart. Each series consists of some amount of graphs split in time. By default, LineChart connects those graphs. It looks ugly and makes no sense in the context, so I want to separate them, but keep the color and the legend. In other words what I want is to remove connection between two specific points. Is there a way to do this without resorting to adding new series to the chart (the graphs are logically connected, and adding new series would confuse the user and clutter the chart)?
for(int j = 0; j < 4; j++) {
XYChart.Series<Float, Float> series = new XYChart.Series<>();
series.setName("Канал " + (j + 1));
fillWithData(series);
chart.getData().add(series);
}
回答1:
Yes you can
As stated in https://docs.oracle.com/javase/8/javafx/api/javafx/scene/chart/XYChart.Series.html#nodeProperty the serie node is use to store the line. the node is a path that you can modify.
look at this small app :
package application;
import java.util.Arrays;
import java.util.List;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.chart.Axis;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.shape.Circle;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.stage.Stage;
public class Main extends Application {
@Override
public void start(Stage primaryStage) {
NumberAxis xAxis = new NumberAxis();
NumberAxis yAxis = new NumberAxis();
MyLineChart chart = new MyLineChart(xAxis, yAxis, Arrays.asList(5));
XYChart.Series<Number, Number> serie = new Series<>();
serie.getData().add(new Data<Number, Number>(0, 5));
serie.getData().add(new Data<Number, Number>(1, 5));
serie.getData().add(new Data<Number, Number>(2, 5));
serie.getData().add(new Data<Number, Number>(3, 5));
serie.getData().add(new Data<Number, Number>(4, 5));
serie.getData().add(new Data<Number, Number>(5, -5));
serie.getData().add(new Data<Number, Number>(6, -5));
serie.getData().add(new Data<Number, Number>(7, -5));
chart.getData().add(serie);
Path p = (Path) serie.getNode();
Scene scene = new Scene(chart,400,400);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
private static class MyLineChart extends LineChart<Number,Number>{
private List<Integer> breakpointIndex;
public MyLineChart(Axis<Number> xAxis, Axis<Number> yAxis, List<Integer> breakpointIndex) {
super(xAxis, yAxis);
this.breakpointIndex = breakpointIndex;
}
@Override
protected void layoutPlotChildren() {
super.layoutPlotChildren();
Path p = (Path) getData().get(0).getNode();
breakpointIndex.forEach(i ->{
Data<Number, Number> discontinuousPoint = getData().get(0).getData().get(i+1);
p.getElements().add(i+1, new MoveTo(getXAxis().getDisplayPosition( discontinuousPoint.getXValue()), getYAxis().getDisplayPosition(discontinuousPoint.getYValue())));
});
System.out.println("\nnew Path :");
p.getElements().forEach(e -> System.out.println("p : " + e));
//getPlotChildren().add(new Circle(50));
}
}
}
You have to override the method layoutplotchildren that is responsible to draw the chart (it's interesting to see the code of the origin method if you can).
and then you can draw what you want here.
In my small app i'am just modifying the node that are already create but you can omit the super call and draw what you want.
look at the doc you have method to access chartelement like getPlotChildren(), getchartchildren().
and you can access to the serie added to the chart with getdata()
回答2:
Apparently in JavaFX 11 internal logic for filling underlying Elements
list in Path
has changed. Now, if I zoom the chart (in other words, change lower/upper bounds on axis), it gets repopulated only with visible elements, so indexes for Data
and PathElement
don't match anymore (and neither do the sizes of respective lists). I guess it's an optimization of sorts.
I was forced to rewrite the code the following hacky way:
@Override
protected void layoutPlotChildren() {
super.layoutPlotChildren();
for(Series<Number, Number> data : getData()) {
ObservableList<PathElement> elements = ((Path)data.getNode()).getElements();
for(int i = 0; i < elements.size() - 1; i++) {
PathElement oldElement = elements.get(i);
PathElement newElement = elements.get(i + 1);
if (oldElement instanceof LineTo && newElement instanceof LineTo) {
double oldDisplayX = ((LineTo) oldElement).getX();
double newDisplayX = ((LineTo) newElement).getX();
if(getXAxis().getValueForDisplay(newDisplayX).doubleValue() -
getXAxis().getValueForDisplay(oldDisplayX).doubleValue() > 10)
elements.set(i, new MoveTo(newDisplayX, ((LineTo) newElement).getY()));
}
}
}
}
Now (for me) there's no need to keep separate list of breakpoint indexes, not is there a need to add them manually, since I insert a breakpoint on one condition only: space between X values is more than 10. I ignore Data
list completely and traverse the Elements
list, transforming display coordinates to X values where needed. For a large number of elements this will add some overhead, obviously, since this gets called each time window size changes, or when new element gets added, but for my use case chart consists of 4 series of 6000-15000 dots and for this size calculation takes ~1ms which is negligible. I could also probably remove one of the getValueForDisplay calls, calculating the difference in pixels first, not sure.
来源:https://stackoverflow.com/questions/38591452/is-there-a-way-to-disconnect-2-dots-in-series-in-javafx-linechart