Performance issue with JavaFX LineChart with 65000 data points

折月煮酒 提交于 2019-11-29 04:25:52

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;
  }

}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!