It takes JavaFX 15 minutes to build described LineChart which doesn't work for my task.
Similar implementation using good old Swing and jFreeChart takes 1.5 seconds to build chart.
But I would still like to implement a JavaFX one.
Here is my code:
public class FXMLController implements Initializable {
@FXML
private Label statusbar;
@FXML
public LineChart lineChart;
@FXML
public Button connect;
@FXML
public MenuItem options;
@FXML
public NumberAxis xAxis;
@FXML
NumberAxis yAxis;
@FXML
private void connect(ActionEvent event) {
}
public static FileChooser fileChooser = new FileChooser();
public static String path;
public static XYChart.Series<Integer, Integer> dataSeries = new XYChart.Series<Integer, Integer>();
public static int y = 0;
public static XYChart.Data<Integer, Integer> data;
@FXML
private void open(ActionEvent event) {
fileChooser.setTitle("Open Resource File");
fileChooser.getExtensionFilters().addAll(
new ExtensionFilter("Text Files", "*.txt"),
new ExtensionFilter("Image Files", "*.png", "*.jpg", "*.gif"),
new ExtensionFilter("Audio Files", "*.wav", "*.mp3", "*.aac"),
new ExtensionFilter("All Files", "*.*"));
File selectedFile = fileChooser.showOpenDialog(new Stage());
if (selectedFile != null) {
path = selectedFile.getAbsolutePath();
System.out.println(path);
try {
ReadingFromFile.readFile(path);
} catch (IOException ex) {
Logger.getLogger(FXMLController.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
@FXML
private void close(ActionEvent event) {
}
@FXML
private void getconnect(ActionEvent event) {
}
@Override
public void initialize(URL url, ResourceBundle rb) {
xAxis.setLabel("Tick");
xAxis.setTickUnit(100);
yAxis.setLabel("Signal");
xAxis.setForceZeroInRange(false);
lineChart.setLegendVisible(false);
lineChart.setCreateSymbols(false);
lineChart.setAnimated(false);
lineChart.getData().add(dataSeries);
}
}
and reading from file:
public class ReadingFromFile extends FXMLController {
public static String s = null;
public static String[] str;
public static int parseInt;
public static void readFile(String filename)
throws IOException {
BufferedReader br = new BufferedReader(new FileReader(filename));
try {
StringBuilder sb = new StringBuilder();
String line = br.readLine();
while (line != null) {
sb.append(line);
sb.append(System.lineSeparator());
line = br.readLine();
System.out.println(line);
try {
str = line.split(" ");
for (int i = 0; i < str.length; i = i + 2) {
s = str[i + 1] + str[i];
parseInt = Integer.parseInt(s, 16);
javafx.application.Platform.runLater(new Runnable() {
@Override
public void run() {
data = new XYChart.Data<Integer, Integer>(y, parseInt);
//data.setNode(new HoveredThresholdNode(0, second, ""));
dataSeries.getData().add(data);
y++;
}
});
}
} catch (java.lang.NullPointerException ex) {
System.out.println("тут ноль!!!");
}
}
} finally {
br.close();
}
}
}
I experienced a similar problem, adding 100,000 points to a LineChart every couple of seconds. We solved it using the Ramer–Douglas–Peucker algorithm, this reduces the number of points in the line without the user noticing. I found an ready-made implementation in the JTS topology suite under LGPL license.
Here's the my test code.
public class ChartUpdate extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) {
NumberAxis xAxis = new NumberAxis(0, 50_000, 5000);
xAxis.setAutoRanging(false);
NumberAxis yAxis = new NumberAxis(-1, 1, 25);
yAxis.setAutoRanging(false);
LineChart<Number, Number> graph = new LineChart<>(xAxis, yAxis);
graph.setAnimated(false);
graph.setCreateSymbols(false);
graph.setLegendVisible(false);
Series<Number, Number> series = new Series<>();
stage.setScene(new Scene(graph));
GeometryFactory gf = new GeometryFactory();
long t0 = System.nanoTime();
Coordinate[] coordinates = new Coordinate[100_000];
for (int i = 0; i < coordinates.length; i++) {
coordinates[i] = new Coordinate(i, Math.sin(Math.toRadians(i / 100)));
}
Geometry geom = new LineString(new CoordinateArraySequence(coordinates), gf);
Geometry simplified = DouglasPeuckerSimplifier.simplify(geom, 0.00001);
List<Data<Number, Number>> update = new ArrayList<Data<Number, Number>>();
for (Coordinate each : simplified.getCoordinates()) {
update.add(new Data<>(each.x, each.y));
}
long t1 = System.nanoTime();
System.out.println(String.format("Reduces points from %d to %d in %.1f ms", coordinates.length, update.size(),
(t1 - t0) / 1e6));
ObservableList<Data<Number, Number>> list = FXCollections.observableArrayList(update);
series.setData(list);
graph.getData().add(series);
stage.show();
}
}
Ramer-Douglas-Peucker is unnecessarily complicated, and even with a faster downsampling strategy, this alone is not enough to get the performance we need. See my answer here for a more complete solution. This has achieved true real-time updating for me with data sets of 40,000 or so.
I recently experienced this problem too. Below is an example of the class with comments. I also create a JavaFX application to test the entered epsilon and number of markers on GitHub. JavaFX Line Chart Example
/**
* Uses the Douglas Peucker algorithm for reducing the series.
* Reference: https://rosettacode.org/wiki/Ramer-Douglas-Peucker_line_simplification#Java
*/
public class SeriesReducer {
private double epsilon;
/**
* Filters the series. This assumes the data set is a map that uses the keys for a line chart
* category axis and uses the values for a line chart number axis.
*
* @param chartDataSet The map containing the chart data set
*/
public Map<String, Integer> filter(Map<String, Integer> chartDataSet) {
List<Entry<String, Integer>> dataSet = new ArrayList<>(chartDataSet.entrySet());
List<Entry<String, Integer>> pointListOut = new ArrayList<>();
reduce(dataSet, pointListOut);
Map<String, Integer> reducedSeriesMap = new TreeMap<>();
pointListOut.forEach(entry -> reducedSeriesMap.put(entry.getKey(), entry.getValue()));
DecimalFormat numberFormat = new DecimalFormat("#.00");
int pointListOutSize = pointListOut.size();
String percentage =
numberFormat.format((1 - ((double) pointListOutSize / (double) dataSet.size())) * 100);
String reducedByMessage = pointListOutSize + " (" + percentage + "%)";
AppViewModel.getInstance().setReducedByMessage((reducedByMessage));
return reducedSeriesMap;
}
/**
* Gets the perpendicular distance.
*
* @param line The line object with the data
* @return The perpendicular distance
*/
private double getPerpendicularDistance(Line line) {
double dx = line.getLineEndX() - line.getLineStartX();
double dy = line.getLineEnd().getValue() - line.getLineStart().getValue();
double mag = Math.hypot(dx, dy);
if (mag > 0.0) {
dx /= mag;
dy /= mag;
}
double pvx = line.getPointX() - line.getLineStartX();
double pvy = line.getPoint().getValue() - line.getLineStart().getValue();
double pvdot = dx * pvx + dy * pvy;
double ax = pvx - pvdot * dx;
double ay = pvy - pvdot * dy;
return Math.hypot(ax, ay);
}
/**
* Reduces the number of points.
*/
private void reduce(List<Entry<String, Integer>> pointList, List<Entry<String, Integer>> listOut) {
int startIndex = 0;
int endIndex = pointList.size() - 1;
int index = 0;
double maxDistance = 0;
for (int i = startIndex + 1; i < endIndex; i++) {
Line line = new Line.Builder()
.setPoint(pointList.get(i))
.setLineStart(pointList.get(startIndex))
.setLineEnd(pointList.get(endIndex))
.setPointX(i)
.setLineStartX(startIndex)
.setLineEndX(endIndex)
.build();
double distance = getPerpendicularDistance(line);
if (distance > maxDistance) {
index = i;
maxDistance = distance;
}
}
if (maxDistance > epsilon) {
List<Entry<String, Integer>> result1 = new ArrayList<>();
List<Entry<String, Integer>> result2 = new ArrayList<>();
List<Entry<String, Integer>> firstLine = pointList.subList(startIndex, index + 1);
List<Entry<String, Integer>> lastLine = pointList.subList(index, pointList.size());
reduce(firstLine, result1);
reduce(lastLine, result2);
List<Entry<String, Integer>> result = new ArrayList<>(result1.size() + result2.size());
result.addAll(result1);
result.addAll(result2);
listOut.addAll(result1.subList(startIndex, result1.size() - 1));
listOut.addAll(result2);
} else {
listOut.clear();
listOut.add(pointList.get(startIndex));
listOut.add(pointList.get(pointList.size() - 1));
}
}
/**
* Sets the threshold epsilon.
*
* @param epsilon The margin for the curve
*/
public void setEpsilon(double epsilon) {
this.epsilon = epsilon;
}
}