Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 1 year ago.
Improve this question
I try to combine Chart-FX in https://github.com/GSI-CS-CO/chart-fx and TableView in JavaFX into one interface to meet my requirements. At present, I have written a demo to realize it. But now I find that when the number of points of the DataSet in the chart is getting more and more, it will block the UI thread and cause the refresh of the TableView to become stagnant. Moreover, my refresh frequency is at the millisecond level (refresh the table every 5ms and refresh the accumulated data to the chart every second). I have tried many methods. First of all, platform. runLater is not an option, because the refresh rate is too high and it will jam the UI thread. Then I try to replace the runLater with a service, which obviously reduces the memory usage. However, the problem of blocking when Chart and TableView refresh at the same time has not been solved. Could you please tell me how to solve this problem?
The code is as follows:
package de.gsi.chart.samples;
import java.time.ZoneOffset;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import de.gsi.chart.plugins.DataPointTooltip;
import de.gsi.chart.plugins.TableViewer;
import de.gsi.chart.plugins.Zoomer;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.gsi.chart.XYChart;
import de.gsi.chart.axes.spi.DefaultNumericAxis;
import de.gsi.chart.axes.spi.format.DefaultTimeFormatter;
import de.gsi.chart.plugins.EditAxis;
import de.gsi.chart.renderer.ErrorStyle;
import de.gsi.chart.renderer.datareduction.DefaultDataReducer;
import de.gsi.chart.renderer.spi.ErrorDataSetRenderer;
import de.gsi.chart.ui.ProfilerInfoBox;
import de.gsi.chart.ui.ProfilerInfoBox.DebugLevel;
import de.gsi.chart.ui.geometry.Side;
import de.gsi.dataset.event.AddedDataEvent;
import de.gsi.dataset.spi.CircularDoubleErrorDataSet;
import de.gsi.dataset.utils.ProcessingProfiler;
/**
* #author rstein
*/
public class RollingBufferSample extends Application {
private static final Logger LOGGER = LoggerFactory.getLogger(RollingBufferSample.class);
public static final int DEBUG_UPDATE_RATE = 5000;
// 0: just drop points that are drawn on the same pixel '3' points need to be at least 3 pixel apart to be drawn
protected static final int MIN_PIXEL_DISTANCE = 0;
public static int N_SAMPLES = 30; // default: 1000000
public static int UPDATE_PERIOD = 1000; // [ms]
public static int BUFFER_CAPACITY = 50000; // 750 samples # 25 Hz <-> 30 s
public final CircularDoubleErrorDataSet rollingBufferDipoleCurrent = new CircularDoubleErrorDataSet(
"dipole current [A]", RollingBufferSample.BUFFER_CAPACITY);
public final CircularDoubleErrorDataSet rollingBufferBeamIntensity = new CircularDoubleErrorDataSet(
"beam intensity [ppp-1]", RollingBufferSample.BUFFER_CAPACITY);
public final CircularDoubleErrorDataSet rollingBufferBeamIntensity2 = new CircularDoubleErrorDataSet(
"beam intensity [ppp-2]", RollingBufferSample.BUFFER_CAPACITY);
public final CircularDoubleErrorDataSet rollingBufferBeamIntensity3 = new CircularDoubleErrorDataSet(
"beam intensity [ppp-3]", RollingBufferSample.BUFFER_CAPACITY);
public final CircularDoubleErrorDataSet rollingBufferBeamIntensity4 = new CircularDoubleErrorDataSet(
"beam intensity [ppp-4]", RollingBufferSample.BUFFER_CAPACITY);
public final CircularDoubleErrorDataSet rollingBufferBeamIntensity5 = new CircularDoubleErrorDataSet(
"beam intensity [ppp-5]", RollingBufferSample.BUFFER_CAPACITY);
public final CircularDoubleErrorDataSet rollingBufferBeamIntensity6 = new CircularDoubleErrorDataSet(
"beam intensity [ppp-6]", RollingBufferSample.BUFFER_CAPACITY);
public final CircularDoubleErrorDataSet rollingBufferBeamIntensity7 = new CircularDoubleErrorDataSet(
"beam intensity [ppp-7]", RollingBufferSample.BUFFER_CAPACITY);
public final CircularDoubleErrorDataSet rollingBufferBeamIntensity8 = new CircularDoubleErrorDataSet(
"beam intensity [ppp-8]", RollingBufferSample.BUFFER_CAPACITY);
public final CircularDoubleErrorDataSet rollingBufferBeamIntensity9 = new CircularDoubleErrorDataSet(
"beam intensity [ppp-9]", RollingBufferSample.BUFFER_CAPACITY);
public final CircularDoubleErrorDataSet rollingBufferBeamIntensity10 = new CircularDoubleErrorDataSet(
"beam intensity [ppp-10]", RollingBufferSample.BUFFER_CAPACITY);
private final ErrorDataSetRenderer beamIntensityRenderer = new ErrorDataSetRenderer();
private final ErrorDataSetRenderer beamIntensityRenderer2 = new ErrorDataSetRenderer();
private final ErrorDataSetRenderer beamIntensityRenderer3 = new ErrorDataSetRenderer();
private final ErrorDataSetRenderer beamIntensityRenderer4 = new ErrorDataSetRenderer();
private final ErrorDataSetRenderer beamIntensityRenderer5 = new ErrorDataSetRenderer();
private final ErrorDataSetRenderer beamIntensityRenderer6 = new ErrorDataSetRenderer();
private final ErrorDataSetRenderer beamIntensityRenderer7 = new ErrorDataSetRenderer();
private final ErrorDataSetRenderer beamIntensityRenderer8 = new ErrorDataSetRenderer();
private final ErrorDataSetRenderer beamIntensityRenderer9 = new ErrorDataSetRenderer();
private final ErrorDataSetRenderer beamIntensityRenderer10 = new ErrorDataSetRenderer();
private final ErrorDataSetRenderer dipoleCurrentRenderer = new ErrorDataSetRenderer();
private final DefaultNumericAxis yAxis1 = new DefaultNumericAxis("beam intensity", "ppp");
private final DefaultNumericAxis yAxis2 = new DefaultNumericAxis("dipole current", "A");
protected Timer[] timer;
private int i=0;
private void generateBeamIntensityData() {
final long startTime = ProcessingProfiler.getTimeStamp();
final double now = System.currentTimeMillis() / 1000.0 + 1;
// N.B. '+1' to check for resolution
if (rollingBufferBeamIntensity.getDataCount() == 0) {
// suppress auto notification since we plan to add multiple data points
// N.B. this is for illustration of the 'setAutoNotification(..)' functionality
// one may use also the add(double[], double[], ...) method instead
boolean oldState = rollingBufferBeamIntensity.autoNotification().getAndSet(false);
for (int n = RollingBufferSample.N_SAMPLES; n >= 0; --n) {
final double t = now - n * RollingBufferSample.UPDATE_PERIOD / 1000.0;
final double y = 100 * RollingBufferSample.rampFunctionBeamIntensity(t);
final double ey = 1;
if(i<3500){
// rollingBufferBeamIntensity.add(t, (int)(Math.random()*1000), ey, ey);
}
i++;
// N.B. update events suppressed by 'setAutoNotification(false)' above
}
rollingBufferBeamIntensity.autoNotification().set(oldState);
// need to issue a separate update notification
rollingBufferBeamIntensity.fireInvalidated(new AddedDataEvent(rollingBufferBeamIntensity));
} else {
final double t = now;
final double y2 = 100 * RollingBufferSample.rampFunctionBeamIntensity(t);
final double ey = 1;
// single add automatically fires update event/update of chart
// rollingBufferBeamIntensity.add(t, (int)(Math.random()*1000), ey, ey);
}
ProcessingProfiler.getTimeDiff(startTime, "adding data into DataSet");
}
private void generateDipoleCurrentData() {
System.out.println(Thread.currentThread().getName());
final long startTime = ProcessingProfiler.getTimeStamp();
final double now = System.currentTimeMillis() / 1000.0 + 1; // N.B. '+1'
// to check
// for
// resolution
if (rollingBufferDipoleCurrent.getDataCount() == 0) {
// suppress auto notification since we plan to add multiple data points
// N.B. this is for illustration of the 'setAutoNotification(..)' functionality
// one may use also the add(double[], double[], ...) method instead
boolean oldState = rollingBufferDipoleCurrent.autoNotification().getAndSet(false);
for (int n = RollingBufferSample.N_SAMPLES; n >= 0; --n) {
final double t = now - n * RollingBufferSample.UPDATE_PERIOD / 1000.0;
final double y = 25 * RollingBufferSample.rampFunctionDipoleCurrent(t);
final double ey = 1;
rollingBufferDipoleCurrent.add(t, (int)(Math.random()*1000), ey, ey);
// N.B. update events suppressed by 'setAutoNotification(false)' above
}
rollingBufferDipoleCurrent.autoNotification().set(oldState);
// need to issue a separate update notification
rollingBufferDipoleCurrent.fireInvalidated(new AddedDataEvent(rollingBufferDipoleCurrent));
} else {
boolean oldState = rollingBufferDipoleCurrent.autoNotification().getAndSet(false);
for (int j = 0; j < 200; j++) {
final double t = now;
final double y = 25 * RollingBufferSample.rampFunctionDipoleCurrent(t);
final double ey = 1;
// single add automatically fires update event/update of chart
rollingBufferBeamIntensity.add(System.currentTimeMillis(), (int)(Math.random()*1000), ey, ey);
rollingBufferBeamIntensity2.add(System.currentTimeMillis(), (int)(Math.random()*1000), ey, ey);
rollingBufferBeamIntensity3.add(System.currentTimeMillis(), (int)(Math.random()*1000), ey, ey);
rollingBufferBeamIntensity4.add(System.currentTimeMillis(), (int)(Math.random()*1000), ey, ey);
rollingBufferBeamIntensity5.add(System.currentTimeMillis(), (int)(Math.random()*1000), ey, ey);
rollingBufferBeamIntensity6.add(System.currentTimeMillis(), (int)(Math.random()*1000), ey, ey);
rollingBufferBeamIntensity7.add(System.currentTimeMillis(), (int)(Math.random()*1000), ey, ey);
rollingBufferBeamIntensity8.add(System.currentTimeMillis(), (int)(Math.random()*1000), ey, ey);
rollingBufferBeamIntensity9.add(System.currentTimeMillis(), (int)(Math.random()*1000), ey, ey);
rollingBufferBeamIntensity10.add(System.currentTimeMillis(), (int)(Math.random()*1000), ey, ey);
System.out.println("当前计数"+i++);
}
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
rollingBufferDipoleCurrent.autoNotification().set(oldState);
// need to issue a separate update notification
rollingBufferDipoleCurrent.fireInvalidated(new AddedDataEvent(rollingBufferDipoleCurrent));
}
ProcessingProfiler.getTimeDiff(startTime, "adding data into DataSet");
}
private HBox getHeaderBar(Scene scene) {
final Button newDataSet = new Button("new DataSet");
newDataSet.setOnAction(evt -> {
// getTask(0).run();
// getTask(1).run();
Service<Integer> service=new Service() {
#Override
protected Task createTask() {
return new Task() {
#Override
protected Integer call() throws Exception {
int i=0;
while (true){
Thread.sleep(1000);
updateValue(i++);
}
}
};
}
};
service.valueProperty().addListener(new ChangeListener<Integer>() {
#Override
public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue) {
generateDipoleCurrentData();
}
});
service.start();
});
final Button startTimer = new Button("timer");
startTimer.setOnAction(evt -> {
if (timer == null) {
timer = new Timer[2];
timer[0] = new Timer("sample-update-timer", true);
rollingBufferBeamIntensity.reset();
timer[0].scheduleAtFixedRate(getTask(0), 0, UPDATE_PERIOD);
timer[1] = new Timer("sample-update-timer", true);
rollingBufferDipoleCurrent.reset();
timer[1].scheduleAtFixedRate(getTask(1), 0, UPDATE_PERIOD);
} else {
timer[0].cancel();
timer[1].cancel();
timer = null; // NOPMD
}
});
// H-Spacer
Region spacer = new Region();
spacer.setMinWidth(Region.USE_PREF_SIZE);
HBox.setHgrow(spacer, Priority.ALWAYS);
final ProfilerInfoBox profilerInfoBox = new ProfilerInfoBox(DEBUG_UPDATE_RATE);
profilerInfoBox.setDebugLevel(DebugLevel.VERSION);
return new HBox(newDataSet, startTimer, spacer, profilerInfoBox);
}
protected TimerTask getTask(final int updateItem) {
return new TimerTask() {
private int updateCount;
#Override
public void run() {
if (updateItem == 0) {
generateBeamIntensityData();
} else {
generateDipoleCurrentData();
}
if (updateCount % 20 == 0 && LOGGER.isDebugEnabled()) {
LOGGER.atDebug().addArgument(updateCount).log("update iteration #{}");
}
updateCount++;
}
};
}
public BorderPane initComponents(Scene scene) {
final BorderPane root = new BorderPane();
generateBeamIntensityData();
generateDipoleCurrentData();
initErrorDataSetRenderer(beamIntensityRenderer);
initErrorDataSetRenderer(beamIntensityRenderer2);
initErrorDataSetRenderer(beamIntensityRenderer3);
initErrorDataSetRenderer(beamIntensityRenderer4);
initErrorDataSetRenderer(beamIntensityRenderer5);
initErrorDataSetRenderer(beamIntensityRenderer6);
initErrorDataSetRenderer(beamIntensityRenderer7);
initErrorDataSetRenderer(beamIntensityRenderer8);
initErrorDataSetRenderer(beamIntensityRenderer9);
initErrorDataSetRenderer(beamIntensityRenderer10);
initErrorDataSetRenderer(dipoleCurrentRenderer);
final DefaultNumericAxis xAxis1 = new DefaultNumericAxis("time");
xAxis1.setAutoRangeRounding(false);
xAxis1.setTickLabelRotation(45);
xAxis1.setMinorTickCount(30);
xAxis1.invertAxis(false);
xAxis1.setTimeAxis(true);
yAxis2.setSide(Side.RIGHT);
yAxis2.setAnimated(false);
// N.B. it's important to set secondary axis on the 2nd renderer before
// adding the renderer to the chart
dipoleCurrentRenderer.getAxes().add(yAxis2);
final XYChart chart = new XYChart(xAxis1, yAxis1);
chart.legendVisibleProperty().set(true);
chart.setAnimated(false);
chart.getRenderers().set(0, beamIntensityRenderer);
// chart.getRenderers().add(beamIntensityRenderer2);
// chart.getRenderers().add(beamIntensityRenderer3);
// chart.getRenderers().add(beamIntensityRenderer4);
// chart.getRenderers().add(beamIntensityRenderer5);
// chart.getRenderers().add(beamIntensityRenderer6);
// chart.getRenderers().add(beamIntensityRenderer7);
// chart.getRenderers().add(beamIntensityRenderer8);
// chart.getRenderers().add(beamIntensityRenderer9);
// chart.getRenderers().add(beamIntensityRenderer10);
chart.getPlugins().add(new EditAxis());
chart.getPlugins().add(new DataPointTooltip());
chart.getPlugins().add(new Zoomer());//工具栏
chart.getPlugins().add(new TableViewer());
beamIntensityRenderer.getDatasets().add(rollingBufferBeamIntensity);
beamIntensityRenderer.getDatasets().add(rollingBufferBeamIntensity2);
beamIntensityRenderer.getDatasets().add(rollingBufferBeamIntensity3);
beamIntensityRenderer.getDatasets().add(rollingBufferBeamIntensity4);
beamIntensityRenderer.getDatasets().add(rollingBufferBeamIntensity5);
beamIntensityRenderer.getDatasets().add(rollingBufferBeamIntensity6);
beamIntensityRenderer.getDatasets().add(rollingBufferBeamIntensity7);
beamIntensityRenderer.getDatasets().add(rollingBufferBeamIntensity8);
beamIntensityRenderer.getDatasets().add(rollingBufferBeamIntensity9);
beamIntensityRenderer.getDatasets().add(rollingBufferBeamIntensity10);
dipoleCurrentRenderer.getDatasets().add(rollingBufferDipoleCurrent);
// set localised time offset
if (xAxis1.isTimeAxis() && xAxis1.getAxisLabelFormatter() instanceof DefaultTimeFormatter) {
final DefaultTimeFormatter axisFormatter = (DefaultTimeFormatter) xAxis1.getAxisLabelFormatter();
axisFormatter.setTimeZoneOffset(ZoneOffset.UTC);
axisFormatter.setTimeZoneOffset(ZoneOffset.ofHoursMinutes(5, 0));
}
yAxis1.setForceZeroInRange(true);
yAxis2.setForceZeroInRange(true);
yAxis1.setAutoRangeRounding(true);
yAxis2.setAutoRangeRounding(true);
// init menu bar
root.setTop(getHeaderBar(scene));
long startTime = ProcessingProfiler.getTimeStamp();
ProcessingProfiler.getTimeDiff(startTime, "adding data to chart");
startTime = ProcessingProfiler.getTimeStamp();
root.setCenter(chart);
TableView<Person> pane = getPane();
root.setBottom(pane);
ProcessingProfiler.getTimeDiff(startTime, "adding chart into StackPane");
return root;
}
protected void initErrorDataSetRenderer(final ErrorDataSetRenderer eRenderer) {
eRenderer.setErrorType(ErrorStyle.ERRORSURFACE);
// for higher performance w/o error bars, enable this for comparing with
// the standard JavaFX charting library (which does not support error
// handling, etc.)
eRenderer.setErrorType(ErrorStyle.NONE);
eRenderer.setDashSize(RollingBufferSample.MIN_PIXEL_DISTANCE); // plot pixel-to-pixel distance
eRenderer.setPointReduction(true);
eRenderer.setDrawMarker(false);
final DefaultDataReducer reductionAlgorithm = (DefaultDataReducer) eRenderer.getRendererDataReducer();
reductionAlgorithm.setMinPointPixelDistance(RollingBufferSample.MIN_PIXEL_DISTANCE);
}
#Override
public void start(final Stage primaryStage) {
ProcessingProfiler.setVerboseOutputState(true);
ProcessingProfiler.setLoggerOutputState(true);
ProcessingProfiler.setDebugState(false);
final BorderPane root = new BorderPane();
final Scene scene = new Scene(root, 1800, 1000);
root.setCenter(initComponents(scene));
final long startTime = ProcessingProfiler.getTimeStamp();
primaryStage.setTitle(this.getClass().getSimpleName());
primaryStage.setScene(scene);
primaryStage.setOnCloseRequest(evt -> Platform.exit());
primaryStage.show();
ProcessingProfiler.getTimeDiff(startTime, "for showing");
}
/**
* #param args the command line arguments
*/
public static void main(final String[] args) {
Application.launch(args);
}
public static double rampFunctionBeamIntensity(final double t) {
final int second = (int) Math.floor(t);
final double subSecond = t - second;
double offset = 0.3;
final double y = (1 - 0.1 * subSecond) * 1e9;
double gate = RollingBufferSample.square(2, subSecond - offset)
* RollingBufferSample.square(1, subSecond - offset);
// every 5th cycle is a booster mode cycle
if (second % 5 == 0) {
offset = 0.1;
gate = Math.pow(RollingBufferSample.square(3, subSecond - offset), 2);
}
if (gate <= 0 || subSecond < offset) {
gate = 0;
}
return gate * y;
}
public static double rampFunctionDipoleCurrent(final double t) {
final int second = (int) Math.floor(t);
final double subSecond = t - second;
double offset = 0.3;
double y = 100 * RollingBufferSample.sine(1, subSecond - offset);
// every 5th cycle is a booster mode cycle
if (second % 5 == 0) {
offset = 0.1;
y = 100 * Math.pow(RollingBufferSample.sine(1.5, subSecond - offset), 2);
}
if (y <= 0 || subSecond < offset) {
y = 0;
}
return y + 10;
}
private static double sine(final double frequency, final double t) {
return Math.sin(2.0 * Math.PI * frequency * t);
}
private static double square(final double frequency, final double t) {
final double sine = 100 * Math.sin(2.0 * Math.PI * frequency * t);
final double squarePoint = Math.signum(sine);
return squarePoint >= 0 ? squarePoint : 0.0;
}
ObservableList<Person> realTimeDataObservableList = FXCollections.observableArrayList();
public TableView<Person> getPane(){
//创建一个表格来模仿实际业务刷新
TableView<Person> tableView = new TableView<>();
TableColumn<Person, String> name = new TableColumn<>("Firstname");
name.setPrefWidth(200);
name.setCellValueFactory(new PropertyValueFactory<>("firstName"));
TableColumn<Person, String> lastName = new TableColumn<>("lastName");
lastName.setPrefWidth(200);
lastName.setCellValueFactory(person -> person.getValue().lastNameProperty());
TableColumn<Person, String> email = new TableColumn<>("email");
email.setPrefWidth(200);
email.setCellValueFactory(new PropertyValueFactory<>("email"));
//noinspection unchecked
tableView.getColumns().addAll(name, lastName, email);
for (int i = 0; i < 10; i++) {
Person person = new Person("firstName" + i, "lastName" + i, "email" + i);
realTimeDataObservableList.add(person);
}
tableView.setItems(realTimeDataObservableList);
//创建一个service用来高频刷新表格
Service<Integer> service = new Service<Integer>() {
#Override
protected Task<Integer> createTask() {
return new Task<Integer>() {
#Override
protected Integer call() throws Exception {
for (int i = 0; i < 1000000000; i++) {
TimeUnit.MILLISECONDS.sleep(1);
updateValue(i);
}
return 1000000000;
}
};
}
};
//监听service的value属性更改
service.valueProperty().addListener((o, oldValue, newValue) -> {
// try {
// Thread.sleep(100);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
for (Person person : realTimeDataObservableList) {
person.setFirstName("firstname" + newValue);
person.setLastName("lastname" + newValue);
person.setEmail("email" + newValue);
// System.out.println("正在更新" + newValue);
}
});
service.stateProperty().addListener(new ChangeListener<Worker.State>() {
#Override
public void changed(ObservableValue<? extends Worker.State> observable, Worker.State oldValue, Worker.State newValue) {
System.out.println(newValue);
}
});
service.start();
return tableView;
}
//tableview里面的工具类
public static class Person {
private final StringProperty firstName;
private final StringProperty lastName;
private final StringProperty email;
private Person(String fName, String lName, String email) {
this.firstName = new SimpleStringProperty(fName);
this.lastName = new SimpleStringProperty(lName);
this.email = new SimpleStringProperty(email);
}
public String getFirstName() {
return firstName.get();
}
public void setFirstName(String fName) {
firstName.set(fName);
}
public StringProperty firstNameProperty() {
return firstName;
}
public String getLastName() {
return lastName.get();
}
public void setLastName(String lName) {
lastName.set(lName);
}
public StringProperty lastNameProperty() {
return lastName;
}
public String getEmail() {
return email.get();
}
public void setEmail(String inMail) {
email.set(inMail);
}
public StringProperty emailProperty() {
return email;
} // if this method is commented out then the tableview will not refresh when the email is set.
}
}``
You should think about reducing your data update rate. It just does not make sense to update at 5 ms if your monitor update rate is just 16.7 ms corresponding to 60 Hz. Even 60 Hz may be too much for a human eye to follow, so why do you want to waste your resources on too high update rates? Technically it might be worth looking at the AnimationTimer class to drive the updates. In contrast to Platform.runlater you get some autothrottle for free there.
Related
I'm trying to write a program with an equalizer, a frequency analyzer and a sound level meter. The model part seems to work very well but I'm experimenting some bugs with the IHM.
My last bug is with the level meter. After a while (from few milliseconds to few seconds), it freezes and don't update anymore. So, here is a (simplified) version of it. I added the runnable part to test and reproduce the bug. Of course, this bug appears sooner when I add other graphical components which also need to refresh very frequently. For example, the frequency analyze is represented by a line-chart with something like 1000 points.
public class LevelMeter2 extends Parent implements Runnable {
private IntegerProperty levelMeterHeight = new SimpleIntegerProperty();
private Rectangle led;
private IntegerProperty height = new SimpleIntegerProperty();
private IntegerProperty width = new SimpleIntegerProperty();
private DoubleProperty linearValue = new SimpleDoubleProperty();
private Color backgroundColor=Color.BLACK;
private double minLinearValue, maxLinearValue;
public LevelMeter2 (int height2, int width2) {
this.height.set(height2);
this.levelMeterHeight.bind(height.multiply(0.9));
this.width.set(width2);
linearValue.set(1.0);
minLinearValue = Math.pow(10, -60.0/100);
maxLinearValue = Math.pow(10, 3.0/100)-minLinearValue;
Rectangle levelMeterShape = new Rectangle();
levelMeterShape.widthProperty().bind(width);
levelMeterShape.heightProperty().bind(height);
levelMeterShape.setStroke(backgroundColor);
this.getChildren().add(levelMeterShape);
led = new Rectangle();
led.widthProperty().bind(width.multiply(0.8));
led.translateXProperty().bind(width.multiply(0.1));
led.heightProperty().bind(levelMeterHeight.multiply(linearValue));
led.setFill(Color.AQUA);
Rotate rotate = new Rotate();
rotate.pivotXProperty().bind(width.multiply(0.8).divide(2));
rotate.pivotYProperty().bind(height.divide(2));
rotate.setAngle(180);
led.getTransforms().add(rotate);
this.getChildren().add(led);
}
public double convertdBToLinearValue (double dB) {
return ((double)Math.round(100 * ((Math.pow(10, dB/100)-minLinearValue)/maxLinearValue)) ) /100 ;
//return (Math.pow(10, dB/100)-minLinearValue)/maxLinearValue;
}
public double convertLinearValueTodB (double linearValue) {
return 100*Math.log10(linearValue*maxLinearValue+minLinearValue);
}
public void setValue (double dB) {
if (dB>3) {
dB=3;
}
linearValue.setValue(convertdBToLinearValue(dB));
}
#Override
public void run() {
int i = 0;
double value=-20;
while (i<1000) {
setValue(value);
value = (Math.random()-0.5)*10+value;
if (value>3) {
value=3;
}
if (value<-60) {
value=-60;
}
i++;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("END OF WHILE");
}
}
And a "Main" to test it :
public class MainGraph extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
HBox pane = new HBox();
LevelMeter2 levelMeter = new LevelMeter2(300,30);
Thread t = new Thread(levelMeter);
pane.getChildren().add(levelMeter);
t.start();
Scene scene = new Scene(pane, 300, 300);
primaryStage.setScene(scene);
primaryStage.setTitle("Test IHM");
primaryStage.setOnCloseRequest( event -> {
System.out.println("FIN");
System.exit(0);
});
primaryStage.show();
}
}
What's wrong with my code ? How can I write a more robust code that will allow me high refresh rates of my IHM ? Or how can I prevent from freezing ?
Thank you for you help.
I would suggest you move away from Threads and use something from JavaFX Animation package. In this example Timeline is used. This code is set to run at a rate of about 60 fps. You can adjust that using Duration.millis().
Main
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
/**
*
* #author blj0011
*/
public class JavaFXApplication342 extends Application
{
#Override
public void start(Stage primaryStage)
{
LevelMeter2 levelMeter = new LevelMeter2(300, 30);
Button button = new Button("Start");
button.setOnAction((event) -> {
switch (button.getText()) {
case "Start":
levelMeter.startAnimation();
button.setText("Stop");
break;
case "Stop":
levelMeter.stopAnimation();
button.setText("Start");
break;
}
});
HBox pane = new HBox(levelMeter, button);
Scene scene = new Scene(pane, 300, 300);
primaryStage.setScene(scene);
primaryStage.setTitle("Test IHM");
primaryStage.setOnCloseRequest(event -> {
System.out.println("FIN");
System.exit(0);
});
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args)
{
launch(args);
}
}
LevelMeter2
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.scene.Parent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Rotate;
import javafx.util.Duration;
public final class LevelMeter2 extends Parent
{
private final IntegerProperty levelMeterHeight = new SimpleIntegerProperty();
Timeline timeline;
double value = -20;
private final Rectangle led;
private final IntegerProperty height = new SimpleIntegerProperty();
private final IntegerProperty width = new SimpleIntegerProperty();
private final DoubleProperty linearValue = new SimpleDoubleProperty();
private final Color backgroundColor = Color.BLACK;
private final double minLinearValue;
private final double maxLinearValue;
public LevelMeter2(int height2, int width2)
{
this.height.set(height2);
this.levelMeterHeight.bind(height.multiply(0.9));
this.width.set(width2);
linearValue.set(1.0);
minLinearValue = Math.pow(10, -60.0 / 100);
maxLinearValue = Math.pow(10, 3.0 / 100) - minLinearValue;
Rectangle levelMeterShape = new Rectangle();
levelMeterShape.widthProperty().bind(width);
levelMeterShape.heightProperty().bind(height);
levelMeterShape.setStroke(backgroundColor);
this.getChildren().add(levelMeterShape);
led = new Rectangle();
led.widthProperty().bind(width.multiply(0.8));
led.translateXProperty().bind(width.multiply(0.1));
led.heightProperty().bind(levelMeterHeight.multiply(linearValue));
led.setFill(Color.AQUA);
Rotate rotate = new Rotate();
rotate.pivotXProperty().bind(width.multiply(0.8).divide(2));
rotate.pivotYProperty().bind(height.divide(2));
rotate.setAngle(180);
led.getTransforms().add(rotate);
getChildren().add(led);
timeline = new Timeline(new KeyFrame(Duration.millis(16), (event) -> {
setValue(value);
value = (Math.random() - 0.5) * 10 + value;
if (value > 3) {
value = 3;
}
if (value < -60) {
value = -60;
}
}));
timeline.setCycleCount(Timeline.INDEFINITE);
}
public double convertdBToLinearValue(double dB)
{
return ((double) Math.round(100 * ((Math.pow(10, dB / 100) - minLinearValue) / maxLinearValue))) / 100;
}
public double convertLinearValueTodB(double linearValue)
{
return 100 * Math.log10(linearValue * maxLinearValue + minLinearValue);
}
public void setValue(double dB)
{
if (dB > 3) {
dB = 3;
}
linearValue.setValue(convertdBToLinearValue(dB));
}
public void startAnimation()
{
timeline.play();
}
public void stopAnimation()
{
timeline.stop();
}
}
Multiple LevelMeters Example:
Main
import java.util.ArrayList;
import java.util.List;
import javafx.animation.ParallelTransition;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
/**
*
* #author blj0011
*/
public class JavaFXApplication342 extends Application
{
#Override
public void start(Stage primaryStage)
{
List<LevelMeter2> levelMeter2s = new ArrayList();
List<Timeline> metersTimelines = new ArrayList();
for (int i = 0; i < 9; i++) {
LevelMeter2 levelMeter2 = new LevelMeter2(300, 30);
levelMeter2s.add(levelMeter2);
metersTimelines.add(levelMeter2.getTimeline());
}
ParallelTransition parallelTransition = new ParallelTransition();
parallelTransition.getChildren().addAll(metersTimelines);
Button button = new Button("Start");
button.setOnAction((event) -> {
switch (button.getText()) {
case "Start":
parallelTransition.play();
button.setText("Stop");
break;
case "Stop":
parallelTransition.stop();
button.setText("Start");
break;
}
});
HBox hBox = new HBox();
hBox.getChildren().addAll(levelMeter2s);
VBox vBox = new VBox(hBox, new StackPane(button));
Scene scene = new Scene(vBox, 300, 350);
primaryStage.setScene(scene);
primaryStage.setTitle("Test IHM");
primaryStage.setOnCloseRequest(event -> {
System.out.println("FIN");
System.exit(0);
});
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args)
{
launch(args);
}
}
LevelMeter2
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.scene.Parent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Rotate;
import javafx.util.Duration;
public final class LevelMeter2 extends Parent
{
private final IntegerProperty levelMeterHeight = new SimpleIntegerProperty();
Timeline timeline;
double value = -20;
private final Rectangle led;
private final IntegerProperty height = new SimpleIntegerProperty();
private final IntegerProperty width = new SimpleIntegerProperty();
private final DoubleProperty linearValue = new SimpleDoubleProperty();
private final Color backgroundColor = Color.BLACK;
private final double minLinearValue;
private final double maxLinearValue;
public LevelMeter2(int height2, int width2)
{
this.height.set(height2);
this.levelMeterHeight.bind(height.multiply(0.9));
this.width.set(width2);
linearValue.set(1.0);
minLinearValue = Math.pow(10, -60.0 / 100);
maxLinearValue = Math.pow(10, 3.0 / 100) - minLinearValue;
Rectangle levelMeterShape = new Rectangle();
levelMeterShape.widthProperty().bind(width);
levelMeterShape.heightProperty().bind(height);
levelMeterShape.setStroke(backgroundColor);
this.getChildren().add(levelMeterShape);
led = new Rectangle();
led.widthProperty().bind(width.multiply(0.8));
led.translateXProperty().bind(width.multiply(0.1));
led.heightProperty().bind(levelMeterHeight.multiply(linearValue));
led.setFill(Color.AQUA);
Rotate rotate = new Rotate();
rotate.pivotXProperty().bind(width.multiply(0.8).divide(2));
rotate.pivotYProperty().bind(height.divide(2));
rotate.setAngle(180);
led.getTransforms().add(rotate);
getChildren().add(led);
timeline = new Timeline(new KeyFrame(Duration.millis(25), (event) -> {
setValue(value);
value = (Math.random() - 0.5) * 10 + value;
if (value > 3) {
value = 3;
}
if (value < -60) {
value = -60;
}
}));
timeline.setCycleCount(Timeline.INDEFINITE);
}
public double convertdBToLinearValue(double dB)
{
return ((double) Math.round(100 * ((Math.pow(10, dB / 100) - minLinearValue) / maxLinearValue))) / 100;
}
public double convertLinearValueTodB(double linearValue)
{
return 100 * Math.log10(linearValue * maxLinearValue + minLinearValue);
}
public void setValue(double dB)
{
if (dB > 3) {
dB = 3;
}
linearValue.setValue(convertdBToLinearValue(dB));
}
public void startAnimation()
{
timeline.play();
}
public void stopAnimation()
{
timeline.stop();
}
public Timeline getTimeline()
{
return timeline;
}
}
Your implementation of run() appears to be updating the scene graph from a background thread. As discussed in Concurrency in JavaFX:
The JavaFX scene graph…is not thread-safe and can only be accessed and modified from the UI thread also known as the JavaFX Application thread. Implementing long-running tasks on the JavaFX Application thread inevitably makes an application UI unresponsive."
Instead, use a Task, illustrated here and here. Your implementation of call() can collect data asynchronously and notify the GUI of the current state via updateValue(). Your valueProperty() listener can then invoke setValue() safely. Because "Updates are coalesced to prevent saturation of the FX event queue," your application will perform satisfactorily even on older hardware.
Alternatively, if your audio source is one of the supported Media types, AudioBarChartApp, also seen here, updates the data model of a BarChart in an AudioSpectrumListener registered with the corresponding MediaPlayer. The image below displays pink noise.
private XYChart.Data<String, Number>[] series1Data;
…
audioSpectrumListener = (double timestamp, double duration,
float[] magnitudes, float[] phases) -> {
for (int i = 0; i < series1Data.length; i++) {
series1Data[i].setYValue(magnitudes[i] + 60);
}
};
I would like to add multiple combo boxes to JavaFX that after the user has selected an item the cost of that item will be displayed under the combo box. Also that the total cost of all the selected items will be displayed at the bottom. I know how to make one combo box that will display the cost of one item selected but can't figure out how to make multiple ones and to display the cost of everything selected
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import javafx.collections.FXCollections;
public class Animals extends Application {
Stage window;
Scene scene;
Button button;
ComboBox<Animal> comboBox = new ComboBox<Animal>();
Text textNamePrice = new Text();
static public TextField[] tfLetters = new TextField[37];
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
window = primaryStage;
window.setTitle("ComboBox ");
button = new Button("Submit");
comboBox = new ComboBox<Animal>();
comboBox.setConverter(new StringConverter<Animal>() {
#Override
public String toString(Animal object) {
return object.getName();
}
#Override
public Animal fromString(String string) {
return null;
}
});
comboBox.setItems(FXCollections.observableArrayList(new Animal("Dog", 30.12), new Animal("Cat", 23.23),
new Animal("Bird", 15.0)));
comboBox.valueProperty().addListener((obs, oldVal, newVal) -> {
String selectionText = "Price of the " + newVal.getName() + " is : " + newVal.getPrice();
System.out.println(selectionText);
textNamePrice.setText(selectionText);
});
VBox layout = new VBox(10);
layout.setPadding(new Insets(60, 60, 60, 60));
layout.getChildren().addAll(comboBox, textNamePrice, button);
scene = new Scene(layout, 500, 350);
window.setScene(scene);
window.show();
}
public class Animal {
private String name;
private Double price;
public Double getPrice() {
return price;
}
public String getName() {
return name;
}
public Animal(String name, Double price) {
this.name = name;
this.price = price;
}
}
}
It's probabls easiest to use a custom Node type AnimalChooser for displaying the ComboBox + price. This way the functionality for one selection+price display can be handled in one place. Also you can provide a price property based on the selection to sum them up from you application class.
The following example places all those AnimalChoosers in an VBox and adds a listener to the child list to add and remove listeners to/from the child list, should it be modified, which would allow you to dynamically add/remove those AnimalChooser to/from the VBox and still get a properly updated sum.
public class Animal {
private final String name;
// primitive type should be prefered here
private final double price;
public double getPrice() {
return price;
}
public String getName() {
return name;
}
public Animal(String name, double price) {
this.name = name;
this.price = price;
}
}
public class AnimalChooser extends VBox {
private final ComboBox<Animal> animalCombo;
private final ReadOnlyDoubleWrapper price;
private final Text text;
public AnimalChooser(ObservableList<Animal> items) {
setSpacing(5);
animalCombo = new ComboBox<>(items);
// converter for using a custom string representation of Animal in the
// combobox
animalCombo.setConverter(new StringConverter<Animal>() {
#Override
public String toString(Animal object) {
return object == null ? "" : object.getName();
}
#Override
public Animal fromString(String string) {
if (string == null || string.isEmpty()) {
return null;
} else {
// find suitable animal from list
Animal animal = null;
for (Animal item : items) {
if (string.equals(item.getName())) {
animal = item;
break;
}
}
return animal;
}
}
});
text = new Text();
price = new ReadOnlyDoubleWrapper();
getChildren().addAll(animalCombo, text);
// bind price value to price property
price.bind(Bindings.createDoubleBinding(new Callable<Double>() {
#Override
public Double call() throws Exception {
Animal animal = animalCombo.getValue();
return animal == null ? 0d : animal.getPrice();
}
}, animalCombo.valueProperty()));
// bind text to content of Text node
text.textProperty().bind(Bindings.when(animalCombo.valueProperty().isNull()).then("").otherwise(price.asString("%.2f $")));
}
public final double getPrice() {
return this.price.get();
}
public final ReadOnlyDoubleProperty priceProperty() {
return this.price.getReadOnlyProperty();
}
}
#Override
public void start(Stage primaryStage) {
VBox animalChoosers = new VBox(20);
ObservableList<Animal> animals = FXCollections.observableArrayList(
new Animal("cat", 1000.99),
new Animal("dog", 20.50),
new Animal("goldfish", 15.22)
);
final DoubleProperty total = new SimpleDoubleProperty();
InvalidationListener listener = new InvalidationListener() {
#Override
public void invalidated(Observable observable) {
double sum = 0d;
for (Node n : animalChoosers.getChildren()) {
AnimalChooser chooser = (AnimalChooser) n;
sum += chooser.getPrice();
}
total.set(sum);
}
};
// just in case you want to add AnimalChooser s dynamially to animalChoosers
animalChoosers.getChildren().addListener(new ListChangeListener<Node>() {
#Override
public void onChanged(ListChangeListener.Change<? extends Node> c) {
while (c.next()) {
// add remove listeners updating the total
for (Node n : c.getRemoved()) {
AnimalChooser chooser = (AnimalChooser) n;
chooser.priceProperty().removeListener(listener);
}
for (Node n : c.getAddedSubList()) {
AnimalChooser chooser = (AnimalChooser) n;
chooser.priceProperty().addListener(listener);
}
}
listener.invalidated(null);
}
});
for (int i = 0; i < 10; i++) {
animalChoosers.getChildren().add(new AnimalChooser(animals));
}
BorderPane root = new BorderPane(animalChoosers);
Text totalText = new Text();
totalText.textProperty().bind(total.asString("total: %.2f $"));
root.setBottom(totalText);
BorderPane.setMargin(totalText, new Insets(20));
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
I have this example of Bar chart in which I want to display data in easy readable format:
import java.text.DecimalFormat;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.chart.AreaChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.Tooltip;
import javafx.stage.Stage;
public class MainApp extends Application
{
private static final int MAX_DATA_POINTS = 50;
private Series series;
private Series series2;
private int xSeriesData = 0;
private ConcurrentLinkedQueue<Number> dataQ = new ConcurrentLinkedQueue<Number>();
private ConcurrentLinkedQueue<Number> dataQ2 = new ConcurrentLinkedQueue<Number>();
private ExecutorService executor;
private AddToQueue addToQueue;
private NumberAxis xAxis;
private void init(Stage primaryStage)
{
xAxis = new NumberAxis(0, MAX_DATA_POINTS, MAX_DATA_POINTS / 10);
xAxis.setForceZeroInRange(false);
xAxis.setAutoRanging(false);
NumberAxis yAxis = new NumberAxis();
yAxis.setAutoRanging(true);
//-- Chart
final AreaChart<Number, Number> sc = new AreaChart<Number, Number>(xAxis, yAxis);
sc.setAnimated(false);
sc.setCreateSymbols(false);
sc.setId("liveAreaChart");
sc.setTitle("Animated Area Chart");
//-- Chart Series
series = new AreaChart.Series<Number, Number>();
series.setName("Area Chart Series");
series2 = new AreaChart.Series<Number, Number>();
series2.setName("Area Chart Series");
sc.getData().addAll(series, series2);
xAxis.setTickLabelsVisible(false);
xAxis.setTickMarkVisible(false);
xAxis.setMinorTickVisible(false);
primaryStage.setScene(new Scene(sc));
}
#Override
public void start(Stage primaryStage) throws Exception
{
init(primaryStage);
primaryStage.show();
//-- Prepare Executor Services
executor = Executors.newCachedThreadPool(new ThreadFactory()
{
#Override
public Thread newThread(Runnable r)
{
Thread thread = new Thread(r);
thread.setDaemon(true);
return thread;
}
});
addToQueue = new AddToQueue();
executor.execute(addToQueue);
//-- Prepare Timeline
prepareTimeline();
}
public static void main(String[] args)
{
launch(args);
}
private class AddToQueue implements Runnable
{
#Override
public void run()
{
try
{
// add a item of random data to queue
dataQ.add(randomInteger());
dataQ2.add(randomInteger());
Thread.sleep(400);
executor.execute(this);
}
catch (InterruptedException ex)
{
Logger.getLogger(MainApp.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
public int randomInteger()
{
int min = 100;
int max = 900000000;
int randomNum = min + (int) (Math.random() * ((max - min) + 1));
return randomNum;
}
private static String readableFileSize(long size)
{
if (size <= 0)
return "0";
final String[] units = new String[]
{
"B", "kB", "MB", "GB", "TB"
};
int digitGroups = (int) (Math.log10(size) / Math.log10(1024));
return new DecimalFormat("#,##0.#").format(size / Math.pow(1024, digitGroups)) + " " + units[digitGroups];
}
//-- Timeline gets called in the JavaFX Main thread
private void prepareTimeline()
{
// Every frame to take any data from queue and add to chart
new AnimationTimer()
{
#Override
public void handle(long now)
{
addDataToSeries();
}
}.start();
}
private void addDataToSeries()
{
for (int i = 0; i < 20; i++)
{ //-- add 20 numbers to the plot+
if (dataQ.isEmpty())
break;
// series.getData().add(new AreaChart.Data(xSeriesData++, dataQ.remove()));
Data data = new AreaChart.Data(xSeriesData++, dataQ.remove());
series.getData().add(data);
data.nodeProperty().addListener(new ChangeListener<Node>()
{
#Override
public void changed(ObservableValue<? extends Node> arg0, Node arg1,
Node arg2)
{
Tooltip t = new Tooltip(data.getYValue().toString() + '\n' + data.getXValue());
Tooltip.install(arg2, t);
data.nodeProperty().removeListener(this);
}
});
if (dataQ2.isEmpty())
break;
series2.getData().add(new AreaChart.Data(xSeriesData, dataQ2.remove()));
}
// remove points to keep us at no more than MAX_DATA_POINTS
if (series.getData().size() > MAX_DATA_POINTS)
{
series.getData().remove(0, series.getData().size() - MAX_DATA_POINTS);
}
// remove points to keep us at no more than MAX_DATA_POINTS
if (series2.getData().size() > MAX_DATA_POINTS)
{
series2.getData().remove(0, series2.getData().size() - MAX_DATA_POINTS);
}
// update
xAxis.setLowerBound(xSeriesData - MAX_DATA_POINTS);
xAxis.setUpperBound(xSeriesData - 1);
}
}
The original values are in bytes. Based on the value I want to convert the value in bytes/GB/megabytes and etc using the method readableFileSize(long size) before I insert the value into the chart. The problem is that this method returns String.
How I can refactor the code in order to use this method?
PS. Based on the proposal I tried this:
import java.text.DecimalFormat;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.chart.AreaChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.Tooltip;
import javafx.stage.Stage;
public class MainApp extends Application
{
private static final int MAX_DATA_POINTS = 50;
private Series series;
private Series series2;
private int xSeriesData = 0;
private ConcurrentLinkedQueue<Number> dataQ = new ConcurrentLinkedQueue<Number>();
private ConcurrentLinkedQueue<Number> dataQ2 = new ConcurrentLinkedQueue<Number>();
private ExecutorService executor;
private AddToQueue addToQueue;
private NumberAxis xAxis;
private NumberAxis yAxis;
private void init(Stage primaryStage)
{
xAxis = new NumberAxis(0, MAX_DATA_POINTS, MAX_DATA_POINTS / 10);
xAxis.setForceZeroInRange(false);
xAxis.setAutoRanging(false);
yAxis = new NumberAxis();
yAxis.setAutoRanging(true);
//-- Chart
final AreaChart<Number, Number> sc = new AreaChart<Number, Number>(xAxis, yAxis);
sc.setAnimated(false);
sc.setCreateSymbols(false);
sc.setId("liveAreaChart");
sc.setTitle("Animated Area Chart");
//-- Chart Series
series = new AreaChart.Series<Number, Number>();
series.setName("Area Chart Series");
series2 = new AreaChart.Series<Number, Number>();
series2.setName("Area Chart Series");
sc.getData().addAll(series, series2);
xAxis.setTickLabelsVisible(false);
xAxis.setTickMarkVisible(false);
xAxis.setMinorTickVisible(false);
primaryStage.setScene(new Scene(sc));
}
#Override
public void start(Stage primaryStage) throws Exception
{
init(primaryStage);
primaryStage.show();
//-- Prepare Executor Services
executor = Executors.newCachedThreadPool(new ThreadFactory()
{
#Override
public Thread newThread(Runnable r)
{
Thread thread = new Thread(r);
thread.setDaemon(true);
return thread;
}
});
addToQueue = new AddToQueue();
executor.execute(addToQueue);
//-- Prepare Timeline
prepareTimeline();
}
public static void main(String[] args)
{
launch(args);
}
private class AddToQueue implements Runnable
{
#Override
public void run()
{
try
{
int size = randomInteger();
final String[] units = new String[]
{
"B", "kB", "MB", "GB", "TB"
};
int digitGroups = (int) (Math.log10(size) / Math.log10(1024));
Long valueOf = Long.valueOf(new DecimalFormat("#,##0.#").format(size / Math.pow(1024, digitGroups)));
yAxis.setTickLabelFormatter(new NumberAxis.DefaultFormatter(yAxis)
{
#Override
public String toString(Number object)
{
return String.format("%6.4f " + units[digitGroups], object);
}
});
// add a item of random data to queue
dataQ.add(valueOf);
dataQ2.add(randomInteger());
Thread.sleep(400);
executor.execute(this);
}
catch (InterruptedException ex)
{
Logger.getLogger(MainApp.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
public int randomInteger()
{
int min = 100;
int max = 900000000;
int randomNum = min + (int) (Math.random() * ((max - min) + 1));
return randomNum;
}
private static String readableFileSize(long size)
{
if (size <= 0)
return "0";
final String[] units = new String[]
{
"B", "kB", "MB", "GB", "TB"
};
int digitGroups = (int) (Math.log10(size) / Math.log10(1024));
return new DecimalFormat("#,##0.#").format(size / Math.pow(1024, digitGroups)) + " " + units[digitGroups];
}
//-- Timeline gets called in the JavaFX Main thread
private void prepareTimeline()
{
// Every frame to take any data from queue and add to chart
new AnimationTimer()
{
#Override
public void handle(long now)
{
addDataToSeries();
}
}.start();
}
private void addDataToSeries()
{
for (int i = 0; i < 20; i++)
{ //-- add 20 numbers to the plot+
if (dataQ.isEmpty())
break;
// series.getData().add(new AreaChart.Data(xSeriesData++, dataQ.remove()));
Data data = new AreaChart.Data(xSeriesData++, dataQ.remove());
series.getData().add(data);
data.nodeProperty().addListener(new ChangeListener<Node>()
{
#Override
public void changed(ObservableValue<? extends Node> arg0, Node arg1,
Node arg2)
{
Tooltip t = new Tooltip(data.getYValue().toString() + '\n' + data.getXValue());
Tooltip.install(arg2, t);
data.nodeProperty().removeListener(this);
}
});
if (dataQ2.isEmpty())
break;
series2.getData().add(new AreaChart.Data(xSeriesData, dataQ2.remove()));
}
// remove points to keep us at no more than MAX_DATA_POINTS
if (series.getData().size() > MAX_DATA_POINTS)
{
series.getData().remove(0, series.getData().size() - MAX_DATA_POINTS);
}
// remove points to keep us at no more than MAX_DATA_POINTS
if (series2.getData().size() > MAX_DATA_POINTS)
{
series2.getData().remove(0, series2.getData().size() - MAX_DATA_POINTS);
}
// update
xAxis.setLowerBound(xSeriesData - MAX_DATA_POINTS);
xAxis.setUpperBound(xSeriesData - 1);
}
}
You can use setTickLabelFormatter for the axis tick labels.
A quick example:
import java.text.DecimalFormat;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.stage.Stage;
import javafx.util.StringConverter;
public class LineChartSample extends Application {
#Override
public void start(Stage stage) {
final NumberAxis xAxis = new NumberAxis();
final NumberAxis yAxis = new NumberAxis();
final LineChart<Number, Number> lineChart = new LineChart<Number, Number>(xAxis, yAxis);
XYChart.Series<Number,Number> series = new XYChart.Series<Number,Number>();
for (int i = 0; i < 10000000; i += 1000) {
series.getData().add(new XYChart.Data(i, i));
}
yAxis.setTickLabelFormatter(new StringConverter<Number>() {
#Override
public String toString(Number object) {
// conversion code from:
// http://stackoverflow.com/questions/13539871/converting-kb-to-mb-gb-tb-dynamicaly
double size = object.doubleValue();
String hrSize = null;
double b = size;
double k = size / 1024.0;
double m = ((size / 1024.0) / 1024.0);
double g = (((size / 1024.0) / 1024.0) / 1024.0);
double t = ((((size / 1024.0) / 1024.0) / 1024.0) / 1024.0);
DecimalFormat dec = new DecimalFormat("0.00");
if (t > 1) {
hrSize = dec.format(t).concat(" TB");
} else if (g > 1) {
hrSize = dec.format(g).concat(" GB");
} else if (m > 1) {
hrSize = dec.format(m).concat(" MB");
} else if (k > 1) {
hrSize = dec.format(k).concat(" KB");
} else {
hrSize = dec.format(b).concat(" Bytes");
}
return hrSize;
}
#Override
public Number fromString(String string) {
// TODO: convert from text to number
return null;
}
});
Scene scene = new Scene(lineChart, 800, 600);
lineChart.getData().add(series);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I want to create dynamic StackedBarChart from Java Object. I tried this code:
Java Object:
public class EventsObj
{
private String date;
private int info;
private int error;
private int warning;
private int critical;
public EventsObj()
{
}
public EventsObj(String date, int info, int error, int warning, int critical)
{
this.date = date;
this.info = info;
this.error = error;
this.warning = warning;
this.critical = critical;
}
public String getDate()
{
return date;
}
public void setDate(String date)
{
this.date = date;
}
public int getInfo()
{
return info;
}
public void setInfo(int info)
{
this.info = info;
}
public int getError()
{
return error;
}
public void setError(int error)
{
this.error = error;
}
public int getWarning()
{
return warning;
}
public void setWarning(int warning)
{
this.warning = warning;
}
public int getCritical()
{
return critical;
}
public void setCritical(int critical)
{
this.critical = critical;
}
}
Java code:
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.StackedBarChart;
import javafx.scene.chart.XYChart;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class MainApp extends Application
{
private StackedBarChart<String, Number> stackedChart;
private List<EventsObj> eventsObj;
#Override
public void start(Stage stage) throws Exception
{
createStackedChart();
List<EventsObj> testData = generateTestData();
addStackedChartData(testData);
HBox hb = new HBox(20);
hb.setPadding(new Insets(10, 20, 20, 40));
hb.getChildren().add(stackedChart);
Scene scene = new Scene(hb);
stage.setTitle("JavaFX and Maven");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args)
{
launch(args);
}
private void createStackedChart()
{
CategoryAxis xAxis = new CategoryAxis();
xAxis.setLabel("Days");
NumberAxis yAxis = new NumberAxis();
stackedChart = new StackedBarChart<>(xAxis, yAxis);
stackedChart.setCategoryGap(20);
stackedChart.setMinSize(500, 400);
stackedChart.setMaxSize(500, 400);
}
private List<EventsObj> generateTestData()
{
eventsObj = new ArrayList<>();
for (int i = 0; i < 20; i++)
{
eventsObj.add(new EventsObj(String.valueOf(randomDate()), random(2, 60), random(2, 60), random(2, 60), random(2, 60)));
}
return eventsObj;
}
public static int random(int lowerBound, int upperBound)
{
return (lowerBound + (int) Math.round(Math.random() * (upperBound - lowerBound)));
}
private LocalDate randomDate()
{
Random random = new Random();
int minDay = (int) LocalDate.of(1900, 1, 1).toEpochDay();
int maxDay = (int) LocalDate.of(2015, 1, 1).toEpochDay();
long randomDay = minDay + random.nextInt(maxDay - minDay);
LocalDate randomBirthDate = LocalDate.ofEpochDay(randomDay);
return randomBirthDate;
}
private void addStackedChartData(List<EventsObj> data)
{
ObservableList<XYChart.Series<String, Number>> observableArrayList = FXCollections.observableArrayList();
ObservableList<XYChart.Series<String, Number>> observabt = FXCollections.observableArrayList();
for (EventsObj data1 : data)
{
final XYChart.Series<String, Number> series1 = new XYChart.Series<>();
EventsObj get = data1;
series1.setName(get.getDate());
series1.getData().add(new XYChart.Data<>(get.getDate(), get.getInfo()));
series1.getData().add(new XYChart.Data<>(get.getDate(), get.getWarning()));
series1.getData().add(new XYChart.Data<>(get.getDate(), get.getCritical()));
series1.getData().add(new XYChart.Data<>(get.getDate(), get.getError()));
observabt.addAll(series1);
}
stackedChart.getData().addAll(observabt);
}
}
I need to generate a StackedBarChart that contains one bar for every day that contains different parts for the event types Info, Warning, Critical and Error. But for some reason I can't get it right. It should be something like this:
Countries should be replaced by dates and years should be replaced by event types.
Can you help me fix the code?
You want to create a chart that has one column per date and each column should contain parts Info, Warning, Error and Critical. To do this you need to create a different series for each event type. However you use date as series name and column name. You need to change this:
private static XYChart.Series<String, Number> createSeries(String name) {
XYChart.Series<String, Number> series = new XYChart.Series<>();
series.setName(name);
return series;
}
private void addStackedChartData(List<EventsObj> data) {
XYChart.Series<String, Number> infoSeries = createSeries("Info");
XYChart.Series<String, Number> warningSeries = createSeries("Warning");
XYChart.Series<String, Number> criticalSeries = createSeries("Critical");
XYChart.Series<String, Number> errorSeries = createSeries("Error");
for (EventsObj data1 : data) {
String date = data1.getDate();
infoSeries.getData().add(new XYChart.Data<>(date, data1.getInfo()));
warningSeries.getData().add(new XYChart.Data<>(date, data1.getWarning()));
errorSeries.getData().add(new XYChart.Data<>(date, data1.getError()));
criticalSeries.getData().add(new XYChart.Data<>(date, data1.getCritical()));
}
stackedChart.getData().setAll(errorSeries, warningSeries, infoSeries, criticalSeries);
}
The series has to be used this way:
series.setName(<Color Key>);
series.getData().add(new XYChart.Data<>(<Column>, <Bar Size>));
I would like to change line color based on ColorPicker input during runtime.
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.animation.AnimationTimer;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.chart.AreaChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.ColorPicker;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class MainApp extends Application
{
private static final int MAX_DATA_POINTS = 50;
private Series series;
private int xSeriesData = 0;
private final ConcurrentLinkedQueue<Number> dataQ = new ConcurrentLinkedQueue<>();
private ExecutorService executor;
private AddToQueue addToQueue;
private Timeline timeline2;
private NumberAxis xAxis;
private NumberAxis yAxis;
private AreaChart<Number, Number> sc;
private void init(Stage primaryStage)
{
xAxis = new NumberAxis(0, MAX_DATA_POINTS, MAX_DATA_POINTS / 10);
xAxis.setForceZeroInRange(false);
xAxis.setAutoRanging(false);
yAxis = new NumberAxis();
yAxis.setAutoRanging(true);
sc = new AreaChart<Number, Number>(xAxis, yAxis);
sc.setAnimated(false);
sc.setId("liveAreaChart");
sc.setTitle("Animated Area Chart");
//-- Chart Series
series = new AreaChart.Series<Number, Number>();
series.setName("Area Chart Series");
sc.getData().add(series);
final ColorPicker colorPicker = new ColorPicker();
colorPicker.setOnAction(new EventHandler()
{
#Override
public void handle(Event t)
{
changeColor(colorPicker.getValue().getRed(), colorPicker.getValue().getGreen(), colorPicker.getValue().getBlue(), colorPicker.getValue().getOpacity());
}
});
VBox root = new VBox(5, colorPicker, sc);
primaryStage.setScene(new Scene(root));
}
private void changeColor(double redColor, double greenColor, double blueColor, double opacity)
{
/* int redColor=0, greenColor=127, blueColor=195;
double opacity=0.4;
*/
sc.setStyle("CHART_COLOR_1: rgb(" + redColor + "," + greenColor + "," + blueColor + ");"
+ "CHART_COLOR_1_TRANS_20: rgba(" + redColor + "," + greenColor + "," + blueColor + ");");
}
#Override
public void start(Stage primaryStage) throws Exception
{
init(primaryStage);
primaryStage.show();
//-- Prepare Executor Services
//-- Prepare Executor Services
executor = Executors.newCachedThreadPool(new ThreadFactory()
{
#Override
public Thread newThread(Runnable r)
{
Thread thread = new Thread(r);
thread.setDaemon(true);
return thread;
}
});
addToQueue = new AddToQueue();
executor.execute(addToQueue);
//-- Prepare Timeline
prepareTimeline();
}
public static void main(String[] args)
{
launch(args);
}
private class AddToQueue implements Runnable
{
#Override
public void run()
{
try
{
// add a item of random data to queue
dataQ.add(Math.random());
Thread.sleep(50);
executor.execute(this);
}
catch (InterruptedException ex)
{
Logger.getLogger(MainApp.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
//-- Timeline gets called in the JavaFX Main thread
private void prepareTimeline()
{
// Every frame to take any data from queue and add to chart
new AnimationTimer()
{
#Override
public void handle(long now)
{
addDataToSeries();
}
}.start();
}
private void addDataToSeries()
{
for (int i = 0; i < 20; i++)
{ //-- add 20 numbers to the plot+
if (dataQ.isEmpty())
break;
series.getData().add(new AreaChart.Data(xSeriesData++, dataQ.remove()));
}
// remove points to keep us at no more than MAX_DATA_POINTS
if (series.getData().size() > MAX_DATA_POINTS)
{
series.getData().remove(0, series.getData().size() - MAX_DATA_POINTS);
}
// update
xAxis.setLowerBound(xSeriesData - MAX_DATA_POINTS);
xAxis.setUpperBound(xSeriesData - 1);
}
}
The part which is most unknown to me is how I can get the color of the line during runtime?
Can you help me to implement this, please?
It will be great if I can chose the color from Context menu.
Update: after I select color the diagram is always black.
You need to convert the RGB value ranges from 0.0 - 0.1 to 0 - 255:
private void changeColor( double redColor, double greenColor, double blueColor, double opacity )
{
int r = ( int ) Math.round( redColor * 255.0 );
int g = ( int ) Math.round( greenColor * 255.0 );
int b = ( int ) Math.round( blueColor * 255.0 );
sc.setStyle( "CHART_COLOR_1: rgb(" + r + "," + g + "," + b + ");"
+ "CHART_COLOR_1_TRANS_20: rgba(" + r + "," + g + "," + b + "," + 0.2 + ");" );
}
You may also set the initial default color of ColorPicker with the one defined in modena.css for CHART_COLOR_1:
final ColorPicker colorPicker = new ColorPicker( Color.web( "#f3622d" ) );