Real-time graphing in Java

36,836

Solution 1

If your variable is updating that fast, there's no point in updating a chart every time.

Have you thought about buffering the variable changes, and refreshing the chart on a different thread, say, every 5s ? You should find that JFreeChart can handle such update rates well.

Since JFreeChart is a normal desktop library, you can integrate it with a standard Swing application very easily. Or, you can use it to chart via a web application (by rendering to a JPEG/PNG etc. JFreeChart can generate image maps automatically as well, so you can use mouseovers etc.)

Solution 2

In order to get your CPU well below 100% and allow your GUI to remain responsive, you have to throttle back your chart updating rate. A maximum update rate of around 24 frames per second makes sense for a real-time chart; any faster is more or less indistinguishable anyway. If your data is coming in faster than that rate, you just need to buffer it in the background and update your chart in the foreground at your desired update rate. In the following example, I use XChart along with a SwingWorker background thread. The data capture is simulated at a rate of one per every 5 ms and the chart is updated at 24 frames per second. This concept should work with JFreeCharts or any other charting library as well with slight modification. Disclaimer: I'm the lead developer of XChart.

import java.util.LinkedList;
import java.util.List;

import javax.swing.SwingWorker;

import org.knowm.xchart.QuickChart;
import org.knowm.xchart.SwingWrapper;
import org.knowm.xchart.XYChart;

/**
 * Creates a real-time chart using SwingWorker
 */
public class SwingWorkerRealTime {

  MySwingWorker mySwingWorker;
  SwingWrapper<XYChart> sw;
  XYChart chart;

  public static void main(String[] args) throws Exception {

    SwingWorkerRealTime swingWorkerRealTime = new SwingWorkerRealTime();
    swingWorkerRealTime.go();
  }

  private void go() {

    // Create Chart
    chart = QuickChart.getChart("SwingWorker XChart Real-time Demo", "Time", "Value", "randomWalk", new double[] { 0 }, new double[] { 0 });
    chart.getStyler().setLegendVisible(false);
    chart.getStyler().setXAxisTicksVisible(false);

    // Show it
    sw = new SwingWrapper<XYChart>(chart);
    sw.displayChart();

    mySwingWorker = new MySwingWorker();
    mySwingWorker.execute();
  }

  private class MySwingWorker extends SwingWorker<Boolean, double[]> {

    LinkedList<Double> fifo = new LinkedList<Double>();

    public MySwingWorker() {

      fifo.add(0.0);
    }

    @Override
    protected Boolean doInBackground() throws Exception {

      while (!isCancelled()) {

        fifo.add(fifo.get(fifo.size() - 1) + Math.random() - .5);
        if (fifo.size() > 500) {
          fifo.removeFirst();
        }

        double[] array = new double[fifo.size()];
        for (int i = 0; i < fifo.size(); i++) {
          array[i] = fifo.get(i);
        }
        publish(array);

        try {
          Thread.sleep(5);
        } catch (InterruptedException e) {
          // eat it. caught when interrupt is called
          System.out.println("MySwingWorker shut down.");
        }

      }

      return true;
    }

    @Override
    protected void process(List<double[]> chunks) {

      System.out.println("number of chunks: " + chunks.size());

      double[] mostRecentDataSet = chunks.get(chunks.size() - 1);

      chart.updateXYSeries("randomWalk", null, mostRecentDataSet, null);
      sw.repaintChart();

      long start = System.currentTimeMillis();
      long duration = System.currentTimeMillis() - start;
      try {
        Thread.sleep(40 - duration); // 40 ms ==> 25fps
        // Thread.sleep(400 - duration); // 40 ms ==> 2.5fps
      } catch (InterruptedException e) {
      }

    }
  }
}

XChart SwingWorker Realtime Java Chart

Solution 3

If the data is updating more often than you can generate the chart, then you should have a task in a separate thread that regenerates the chart, and starts another regeneration when it's done. There's little point in running it ore often than that, but if it turns out to be too much of a cpu load, you can throttle back the frequency with which it restarts. If updates don't come in, you don't trigger the re-generate. I did something like that in my Zocalo project recently. It does everything but the throttling back.

package net.commerce.zocalo.freechart;

// Copyright 2009 Chris Hibbert.  All rights reserved.

// This software is published under the terms of the MIT license, a copy
// of which has been included with this distribution in the LICENSE file.

import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.Map;
import java.util.HashMap;

/**  Schedule a task like generating a price history graph.  Multiple requests may come
 in sporadically.  We want to ensure that only one is being processed at a time.  If we're
 busy processing when a request comes in, we'll remember to start another when this one is
 done.  Multiple requests that come in while processing will spur a single restart. */
public class ChartScheduler {
    static private Logger log = Logger.getLogger(ChartScheduler.class);
    static private Map<String, ChartScheduler> schedulers = new HashMap<String, ChartScheduler>();
    private AtomicBoolean generating = new AtomicBoolean(false);
    private AtomicBoolean requested = new AtomicBoolean(false);
    private ExecutorService threads = Executors.newCachedThreadPool();
    private Callable<Boolean> callable;
    private int runs = 0;
    private String name;


    private ChartScheduler(String name, final Runnable worker) {
        this.name = name;
        callable = new Callable<Boolean>() {
            public Boolean call() throws Exception {
                worker.run();
                runs++;
                restartIfNeeded();
                return true;
            }
        };
    }

    public static ChartScheduler create(String name, Runnable worker) {
        ChartScheduler sched = find(name);
        if (sched == null) {
            sched = new ChartScheduler(name, worker);
            schedulers.put(name, sched);
        }
        return sched;
    }

    public static ChartScheduler find(String name) {
        return schedulers.get(name);
    }

    public boolean generateNewChart() {
        requested.set(true);
        if (generating.compareAndSet(false, true)) {
            startNewThread();
            return true;
        } else {
            return false;
        }
    }

    private Future<Boolean> startNewThread() {
        generating.set(true);
        requested.set(false);

        return threads.submit(callable);
    }

    private boolean restartIfNeeded() {
        generating.set(false);
        if (requested.get()) {
            return generateNewChart();

        } else {
            return false;
        }
    }

    public boolean isBusy() {
        return generating.get();
    }

    public int runs() {
        return runs;
    }
}

Solution 4

According to this blog post:

http://jonathanwatmough.com/2008/02/prototyping-code-in-clojure/

its possible to implement 'real-ish time' display of audio spectrums using the KJ DSP library:

http://sirk.sytes.net/software/libs/kjdss/index.htm

so if you can get by with fairly simple charts it might be an alternative to JFreeChart.

Share:
36,836
thodinc
Author by

thodinc

Updated on July 03, 2021

Comments

  • thodinc
    thodinc almost 3 years

    I have an application which updates a variable about between 5 to 50 times a second and I am looking for some way of drawing a continuous XY plot of this change in real-time.

    Though JFreeChart is not recommended for such a high update rate, many users still say that it works for them. I've tried using this demo and modified it to display a random variable, but it seems to use up 100% CPU usage all the time. Even if I ignore that, I do not want to be restricted to JFreeChart's ui class for constructing forms (though I'm not sure what its capabilities are exactly). Would it be possible to integrate it with Java's "forms" and drop-down menus? (as are available in VB) Otherwise, are there any alternatives I could look into?

    EDIT: I'm new to Swing, so I've put together a code just to test the functionality of JFreeChart with it (while avoiding the use of the ApplicationFrame class of JFree since I'm not sure how that will work with Swing's combo boxes and buttons). Right now, the graph is being updated immediately and CPU usage is high. Would it be possible to buffer the value with new Millisecond() and update it maybe twice a second? Also, can I add other components to the rest of the JFrame without disrupting JFreeChart? How would I do that? frame.getContentPane().add(new Button("Click")) seems to overwrite the graph.

    package graphtest;
    
    import java.util.Random;
    import javax.swing.JFrame;
    import org.jfree.chart.ChartFactory;
    import org.jfree.chart.ChartPanel;
    import org.jfree.chart.JFreeChart;
    import org.jfree.chart.axis.ValueAxis;
    import org.jfree.chart.plot.XYPlot;
    import org.jfree.data.time.Millisecond;
    import org.jfree.data.time.TimeSeries;
    import org.jfree.data.time.TimeSeriesCollection;
    
    public class Main {
        static TimeSeries ts = new TimeSeries("data", Millisecond.class);
    
        public static void main(String[] args) throws InterruptedException {
            gen myGen = new gen();
            new Thread(myGen).start();
    
            TimeSeriesCollection dataset = new TimeSeriesCollection(ts);
            JFreeChart chart = ChartFactory.createTimeSeriesChart(
                "GraphTest",
                "Time",
                "Value",
                dataset,
                true,
                true,
                false
            );
            final XYPlot plot = chart.getXYPlot();
            ValueAxis axis = plot.getDomainAxis();
            axis.setAutoRange(true);
            axis.setFixedAutoRange(60000.0);
    
            JFrame frame = new JFrame("GraphTest");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            ChartPanel label = new ChartPanel(chart);
            frame.getContentPane().add(label);
            //Suppose I add combo boxes and buttons here later
    
            frame.pack();
            frame.setVisible(true);
        }
    
        static class gen implements Runnable {
            private Random randGen = new Random();
    
            public void run() {
                while(true) {
                    int num = randGen.nextInt(1000);
                    System.out.println(num);
                    ts.addOrUpdate(new Millisecond(), num);
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException ex) {
                        System.out.println(ex);
                    }
                }
            }
        }
    
    }