I have a problem with JavaFX; I explain: I have a class with 5 series to be plotted but 4 are of scatterchart (points) and a Linechart 1 (solid line). The line chart has continued lower parameters on the X and Y axis higher than the other four series and therefore I need to do a little more calculation. I insert the code of the controller class that draws the graph:
public class ControllerFormantsSmooth {
public static void plotFormantSmooth(String path, Stage stage, String name) {
final NumberAxis xAxis = new NumberAxis(); //asseX
final NumberAxis yAxis = new NumberAxis(); //asseY
//POINT GRAPH
final ScatterChart<Number, Number> scatterFormant = new ScatterChart<Number, Number>(xAxis, yAxis);
//SOLID LINE GRAPH
final LineChart<Number, Number> lineChartFull = new LineChart<Number, Number>(xAxis, yAxis);
ArrayList<Double> extractedF1;
ArrayList<Double> extractedF2;
ArrayList<Double> extractedF3;
ArrayList<Double> extractedF4;
ArrayList<Double> extractedTimeF1;
ArrayList<Double> extractedData;
ArrayList<Double> extractedTime;
//Gestione e settaggio del grafico
scatterFormant.getData().clear();
scatterFormant.setOpacity(0.8);
scatterFormant.getStyleClass().add("CSSformant");
scatterFormant.setAnimated(true);
scatterFormant.autosize();
xAxis.setLabel("Time(ms)");
yAxis.setLabel("Formant(Hz)");
//Gestione e settaggio del grafico
lineChartFull.getData().clear();
lineChartFull.setOpacity(0.8);
lineChartFull.setCreateSymbols(false);
try {
//POPULATE LIST
extractedF1 = FormantExtractor.pointF1();
extractedF2 = FormantExtractor.pointF2();
extractedF3 = FormantExtractor.pointF3();
extractedF4 = FormantExtractor.pointF4();
extractedTimeF1 = FormantExtractor.timeF1();
AudioWaveformCreator.waveForm(path);
extractedData = AudioWaveformCreator.audioPoints();
extractedTime = AudioWaveformCreator.pointTimeOscillo();
//POINT SERIES
XYChart.Series<Number, Number> seriesF1 = new XYChart.Series<Number, Number>();
XYChart.Series<Number, Number> seriesF2 = new XYChart.Series<Number, Number>();
XYChart.Series<Number, Number> seriesF3 = new XYChart.Series<Number, Number>();
XYChart.Series<Number, Number> seriesF4 = new XYChart.Series<Number, Number>();
//LINE SERIE
XYChart.Series<Number, Number> series = new XYChart.Series<Number, Number>();
/*
* A ogni serie viene aggiunta una formante
*/
for (int i = 0; i < extractedF1.size()-1; i++) {
if(extractedF1.get(i)+1>extractedF1.get(i+1) && extractedF1.get(i+1)+1>extractedF1.get(i) &&
extractedF1.get(i)<FormantExtractor.getF1_avg()+50 && extractedF1.get(i)>FormantExtractor.getF1_avg()-50){
seriesF1.getData().add(new XYChart.Data<Number, Number>(extractedTimeF1.get(i), extractedF1.get(i)));
}
}
for (int i = 0; i < extractedF2.size()-1; i++) {
if(extractedF2.get(i)+10>extractedF2.get(i+1) && extractedF2.get(i+1)+10>extractedF2.get(i) &&
extractedF2.get(i)<FormantExtractor.getF2_avg()+50 && extractedF2.get(i)>FormantExtractor.getF2_avg()-50){
seriesF2.getData().add(new XYChart.Data<Number, Number>(extractedTimeF2.get(i), extractedF2.get(i)));
}
}
for (int i = 0; i < extractedF3.size()-1; i++) {
if(extractedF3.get(i)+10>extractedF3.get(i+1) && extractedF3.get(i+1)+10>extractedF3.get(i)
&& extractedF3.get(i)<FormantExtractor.getF3_avg()+50 && extractedF3.get(i)>FormantExtractor.getF3_avg()-50){
seriesF3.getData().add(new XYChart.Data<Number, Number>(extractedTimeF3.get(i), extractedF3.get(i)));
}
}
for (int i = 0; i < extractedF4.size()-1; i++) {
if(extractedF4.get(i)+10>extractedF4.get(i+1) && extractedF4.get(i+1)+10>extractedF4.get(i)
&& extractedF4.get(i)<FormantExtractor.getF4_avg()+50 && extractedF4.get(i)>FormantExtractor.getF4_avg()-50){
seriesF4.getData().add(new XYChart.Data<Number, Number>(extractedTimeF4.get(i), extractedF4.get(i)));
}
}
for (int i = 0; i < extractedData.size(); i= i+100) {
series.getData().add(new XYChart.Data<Number, Number>(extractedTime.get(i)*2550, (extractedData.get(i)/10)));
}
scatterFormant.getData().addAll(seriesF1,seriesF2,seriesF3,seriesF4);
lineChartFull.getData().add(series);
StackPane stackpane = new StackPane();
stackpane.getChildren().addAll(lineChartFull,scatterFormant);
//CREATE WINDOW
Scene scene = new Scene(stackpane, 1000, 600);
scatterFormant.getStylesheets().add("application/application.css");
stage.setScene(scene);
stage.show();
scatterFormant.setLegendVisible(false);
//lineChartFull.setLegendVisible(false);;
} catch (java.lang.Exception e) {
e.printStackTrace();
}
}
}
Then the window here is this:
And it is correct (just what I want!)
But if I resize the window (magnify) the result is this (graph disappears by a solid line):
I just can not understand why ... you have ideas? Thanks in advance to those who want to help me.
It seems a problem with the layout of the charts.
You could replace the container StackPane with a custom class like this:
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.layout.StackPane;
/**
* Container calss for two overlayed charts
*/
public class StackedCharts extends StackPane {
private Parent _bottomNode = null;
private Parent _topNode = null;
/**
* adds node as first child (i.e. bottom)
*
* #param pNode
*/
void addBottom(Parent pNode) {
_bottomNode = pNode;
getChildren().add(0, pNode);
}
/**
* adds node as second child (i.e. top)
*
* #param pNode
*/
void addTop(Parent pNode) {
_topNode = pNode;
getChildren().add(pNode);
}
public Node getBottomNode() {
return _bottomNode;
}
public Node getTopNode() {
return _topNode;
}
/**
* Corrects the problem with overlayed charts
*/
#Override
protected void layoutChildren() {
_topNode.layout();
_bottomNode.layout();
super.layoutChildren();
}
}
and then instead of
StackPane stackpane = new StackPane();
stackpane.getChildren().addAll(lineChartFull,scatterFormant);
use
StackedCharts stackpane = new StackedCharts();
stackpane.addBottom(lineChartFull);
stackpane.addTop(scatterFormant);
Related
I would like to plot one serie per chart, and in the end all the series together in one chart, but did not succeed. I'm asking for help. The code is simple and straight forward. Here is my code:
The main class;
public class TestChart extends Application {
GenPlots genPlots =new GenPlots();
#Override
public void start(Stage primaryStage) {
Button btn = new Button();
btn.setText("Say 'Hello World'");
btn.setOnAction(event -> {
genPlots.GenPlots("Hello");
});
StackPane root = new StackPane();
root.getChildren().add(btn);
Scene scene = new Scene(root, 200, 250);
primaryStage.setTitle("TestCharts");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
And the class aimed to generate the series and the charts:
public class GenPlots {
public GenPlots() {};
Axis xAxis = new NumberAxis();
Axis yAxis = new NumberAxis();
LineChart<Number, Number> lineChart = new LineChart<Number, Number>
(xAxis, yAxis);
LineChart<Number, Number> lineChartMulti = new LineChart<Number,
Number>(xAxis, yAxis);
String serName="*";
// generate the linecharts
public void GenPlots (String hello) {
lineChart.getData().clear();
lineChartMulti.getData().clear();
for (int j=1; j<4;j++) {
XYChart.Series serSIF = new XYChart.Series();
serSIF=getSeries();
serName=String.valueOf(j);
serSIF.setName("Only one "+serName);
lineChart.getData().add(serSIF);
displayChart(lineChart,serName);
lineChartMulti.getData().add(serSIF);
}
displayChart(lineChartMulti,serName+"All Series");
} // end method
// get the series with values - sample
public XYChart.Series getSeries()
{
double x=0.0;
double fx=0.0;
XYChart.Series serL = new XYChart.Series();
for (int k=1; k<5;k++)
{
x=x+2;
fx=x*x*j;
serL.getData().add(new XYChart.Data(x,fx));
}
return serL;
}
// plot the lineCharts
public void displayChart( LineChart<Number, Number>lineChart, String
chartTitle )
{
Stage window = new Stage();
window.initModality(Modality.NONE);
StackPane vb = new StackPane();
vb.setPadding(new Insets(20, 20, 20, 20));
lineChart.setTitle(chartTitle);
vb.getChildren().add(lineChart);
Scene scene = new Scene(vb,500,600);
window.setScene(scene);
window.show();
}
}
Also, the last plots with all series are showing correctly, but the other ones , - one serie per chart - are distorted , or not plotted at all. It seems that the series are resetted to null each time a linechart is generated. I thinks is due to the that series are observable, but I can not figure out how to resolve this problem. Ask kindly for your contribution
I found the solution, which could be usefull for other people:
save the series in a ObservableList-
ObservableList<XYChart.Series<Number,Number>> ser = FXCollections.observableArrayList();
If needed do not clear the series itself, rather the ObservableList.
i need to set hover over the bar in barchart.
I have code which show barchart and I need to show values from y-axis when crossing over bar on graphic.
XYChart.Series set1 = new XYChart.Series<>();
XYChart.Series series1 = new XYChart.Series();
for(int i = 0; i< suply.size(); i++)
{
set1.getData().add(new XYChart.Data(suply.get(i).getName(), Float.parseFloat(suply.get(i).getNum())));
final XYChart.Data<String, Number> data = new XYChart.Data(suply.get(i).getName(), Float.parseFloat(suply.get(i).getNum()));
data.nodeProperty().addListener(new ChangeListener<Node>() {
#Override public void changed(ObservableValue<? extends Node> ov, Node oldNode, Node newNode) {
if (newNode != null) {
}
}
});
series1.getData().add(data);
}
barChart.getData().add(series1);
//now you can get the nodes.
for (Series<String, Number> serie: barChart.getData()){
for (XYChart.Data<String, Number> item: serie.getData()){
item.getNode().setOnMousePressed((MouseEvent event) -> {
System.out.println("you clicked "+item.toString()+serie.toString());
});
}
}
barChart.setCursor(Cursor.CROSSHAIR);
Where to put code for hover on barchart and how?
I found solutions
for (final Series<String, Number> series : barChart.getData()) {
for (final XYChart.Data<String, Number> data : series.getData()) {
Tooltip tooltip = new Tooltip();
tooltip.setText(data.getXValue().toString() +" "+
data.getYValue().toString());
Tooltip.install(data.getNode(), tooltip);
}
}
I am using JavaFX LineChart. I am trying to set the tooltip on the linechart. I am getting error runtime error in event handle. What is wrong in the code?
public class ChartPlot
{
static LineChart <Number,Number> linechart;
static double[] xArray, yArray;
static ArrayList <Double> xList, yList;
public static XYChart.Series series;
public static LineChart linePlot(double[] x, double[] y) {
xArray = new double[x.length];
yArray = new double[y.length];
xArray = x;
yArray = y;
//Defining the x axis
final NumberAxis xAxis = new NumberAxis();
xAxis.setLabel("Wavelength");
//Defining the y axis
final NumberAxis yAxis = new NumberAxis();
yAxis.setLabel("Intensity");
//Creating the line chart
linechart = new LineChart <>(xAxis, yAxis);
linechart.getData().clear();
//Prepare XYChart.Series objects by setting data
series = new XYChart.Series();
//Setting the data to Line chart
for (int i = 0; i < xArray.length; i++) {
series.getData().add(new XYChart.Data(xArray[i], yArray[i]));
}
linechart.setCreateSymbols(false);
linechart.getData().add(series);
xAxis.setAutoRanging(true);
xAxis.setForceZeroInRange(false);
yAxis.setAutoRanging(true);
yAxis.setForceZeroInRange(false);
linechart.autosize();
linechart.applyCss();
String css = FXMLDocumentController.class.getResource("LSG.css").toExternalForm();
linechart.getStylesheets().add(css);
linechart.setLegendVisible(false);
for (XYChart.Series <Number, Number> s: linechart.getData()) {
for (XYChart.Data <Number, Number> d: s.getData()) {
Tooltip.install(d.getNode(), new Tooltip(
d.getXValue().toString() + "," + d.getYValue()));
//Adding Class on Hover or on click
d.getNode().addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler < MouseEvent > () //----- This is the line I am getting error
{#Override
public void handle(MouseEvent event) {
System.out.println("X :" + d.getXValue() + " Y :" + d.getYValue());
}
});
}
}
return linechart;
}
}
I am using Open button to read a file and plot. While plotting I am trying to insert listener also to show the value on which I click.
Peace be upon you!
How to draw multiple axis using JavaFX charts API?
Following is an image of a trend in a huge software where some real time as well as history data is being plotted. There are two pens registered and separate axis for each pen is defined on the same trend.
I have been trying to do exactly this in JavaFX 2.0 charts. I have been able to plot a real time chart which is as follows:
I have been researching about multiple axis using JavaFX and could find the this link but I think this is an older version of JavaFX using FXML. However, I am using JavaFX regular classes to accomplish this.
HELP!!!
Here you can find my solution - MultipleAxesLineChart. It is not generic and just fits my needs, but I think it can give a good sight of how it can be done by using StackPane.
Place two charts in a StackPane.
Use css lookups on the top chart to translate (using translate-x and translate-y) it's axes and labels so that they can be read independently of the bottom chart. Leave the top chart's data plot so that it overlays the bottom chart. Modify colors and legends (or mix chart styles, e.g. line and bar) so that it is obvious which data plot belongs to which series.
Some of the techniques above are demonstrated:
In the answer to: Move tick label JavaFx 2 (which shows how to draw multiple axes on what seems to be a single chart).
In this sample code which layers a linechart on a barchart (but without the axis translation).
public class MultipleAxesLineChart extends StackPane {
private final LineChart baseChart;
private final ObservableList<LineChart> backgroundCharts = FXCollections.observableArrayList();
private final Map<LineChart, Color> chartColorMap = new HashMap<>();
private final double yAxisWidth = 60;
private final AnchorPane detailsWindow;
private final double yAxisSeparation = 20;
private double strokeWidth = 0.3;
public MultipleAxesLineChart(LineChart baseChart, Color lineColor) {
this(baseChart, lineColor, null);
}
public MultipleAxesLineChart(LineChart baseChart, Color lineColor, Double strokeWidth) {
if (strokeWidth != null) {
this.strokeWidth = strokeWidth;
}
this.baseChart = baseChart;
chartColorMap.put(baseChart, lineColor);
styleBaseChart(baseChart);
styleChartLine(baseChart, lineColor);
setFixedAxisWidth(baseChart);
setAlignment(Pos.CENTER_LEFT);
backgroundCharts.addListener((Observable observable) -> rebuildChart());
detailsWindow = new AnchorPane();
bindMouseEvents(baseChart, this.strokeWidth);
rebuildChart();
}
private void bindMouseEvents(LineChart baseChart, Double strokeWidth) {
final DetailsPopup detailsPopup = new DetailsPopup();
getChildren().add(detailsWindow);
detailsWindow.getChildren().add(detailsPopup);
detailsWindow.prefHeightProperty().bind(heightProperty());
detailsWindow.prefWidthProperty().bind(widthProperty());
detailsWindow.setMouseTransparent(true);
setOnMouseMoved(null);
setMouseTransparent(false);
final Axis xAxis = baseChart.getXAxis();
final Axis yAxis = baseChart.getYAxis();
final Line xLine = new Line();
final Line yLine = new Line();
yLine.setFill(Color.GRAY);
xLine.setFill(Color.GRAY);
yLine.setStrokeWidth(strokeWidth/2);
xLine.setStrokeWidth(strokeWidth/2);
xLine.setVisible(false);
yLine.setVisible(false);
final Node chartBackground = baseChart.lookup(".chart-plot-background");
for (Node n: chartBackground.getParent().getChildrenUnmodifiable()) {
if (n != chartBackground && n != xAxis && n != yAxis) {
n.setMouseTransparent(true);
}
}
chartBackground.setCursor(Cursor.CROSSHAIR);
chartBackground.setOnMouseEntered((event) -> {
chartBackground.getOnMouseMoved().handle(event);
detailsPopup.setVisible(true);
xLine.setVisible(true);
yLine.setVisible(true);
detailsWindow.getChildren().addAll(xLine, yLine);
});
chartBackground.setOnMouseExited((event) -> {
detailsPopup.setVisible(false);
xLine.setVisible(false);
yLine.setVisible(false);
detailsWindow.getChildren().removeAll(xLine, yLine);
});
chartBackground.setOnMouseMoved(event -> {
double x = event.getX() + chartBackground.getLayoutX();
double y = event.getY() + chartBackground.getLayoutY();
xLine.setStartX(10);
xLine.setEndX(detailsWindow.getWidth()-10);
xLine.setStartY(y+5);
xLine.setEndY(y+5);
yLine.setStartX(x+5);
yLine.setEndX(x+5);
yLine.setStartY(10);
yLine.setEndY(detailsWindow.getHeight()-10);
detailsPopup.showChartDescrpition(event);
if (y + detailsPopup.getHeight() + 10 < getHeight()) {
AnchorPane.setTopAnchor(detailsPopup, y+10);
} else {
AnchorPane.setTopAnchor(detailsPopup, y-10-detailsPopup.getHeight());
}
if (x + detailsPopup.getWidth() + 10 < getWidth()) {
AnchorPane.setLeftAnchor(detailsPopup, x+10);
} else {
AnchorPane.setLeftAnchor(detailsPopup, x-10-detailsPopup.getWidth());
}
});
}
private void styleBaseChart(LineChart baseChart) {
baseChart.setCreateSymbols(false);
baseChart.setLegendVisible(false);
baseChart.getXAxis().setAutoRanging(false);
baseChart.getXAxis().setAnimated(false);
baseChart.getYAxis().setAnimated(false);
}
private void setFixedAxisWidth(LineChart chart) {
chart.getYAxis().setPrefWidth(yAxisWidth);
chart.getYAxis().setMaxWidth(yAxisWidth);
}
private void rebuildChart() {
getChildren().clear();
getChildren().add(resizeBaseChart(baseChart));
for (LineChart lineChart : backgroundCharts) {
getChildren().add(resizeBackgroundChart(lineChart));
}
getChildren().add(detailsWindow);
}
private Node resizeBaseChart(LineChart lineChart) {
HBox hBox = new HBox(lineChart);
hBox.setAlignment(Pos.CENTER_LEFT);
hBox.prefHeightProperty().bind(heightProperty());
hBox.prefWidthProperty().bind(widthProperty());
lineChart.minWidthProperty().bind(widthProperty().subtract((yAxisWidth+yAxisSeparation)*backgroundCharts.size()));
lineChart.prefWidthProperty().bind(widthProperty().subtract((yAxisWidth+yAxisSeparation)*backgroundCharts.size()));
lineChart.maxWidthProperty().bind(widthProperty().subtract((yAxisWidth+yAxisSeparation)*backgroundCharts.size()));
return lineChart;
}
private Node resizeBackgroundChart(LineChart lineChart) {
HBox hBox = new HBox(lineChart);
hBox.setAlignment(Pos.CENTER_LEFT);
hBox.prefHeightProperty().bind(heightProperty());
hBox.prefWidthProperty().bind(widthProperty());
hBox.setMouseTransparent(true);
lineChart.minWidthProperty().bind(widthProperty().subtract((yAxisWidth + yAxisSeparation) * backgroundCharts.size()));
lineChart.prefWidthProperty().bind(widthProperty().subtract((yAxisWidth + yAxisSeparation) * backgroundCharts.size()));
lineChart.maxWidthProperty().bind(widthProperty().subtract((yAxisWidth + yAxisSeparation) * backgroundCharts.size()));
lineChart.translateXProperty().bind(baseChart.getYAxis().widthProperty());
lineChart.getYAxis().setTranslateX((yAxisWidth + yAxisSeparation) * backgroundCharts.indexOf(lineChart));
return hBox;
}
public void addSeries(XYChart.Series series, Color lineColor) {
NumberAxis yAxis = new NumberAxis();
NumberAxis xAxis = new NumberAxis();
// style x-axis
xAxis.setAutoRanging(false);
xAxis.setVisible(false);
xAxis.setOpacity(0.0); // somehow the upper setVisible does not work
xAxis.lowerBoundProperty().bind(((NumberAxis) baseChart.getXAxis()).lowerBoundProperty());
xAxis.upperBoundProperty().bind(((NumberAxis) baseChart.getXAxis()).upperBoundProperty());
xAxis.tickUnitProperty().bind(((NumberAxis) baseChart.getXAxis()).tickUnitProperty());
// style y-axis
yAxis.setSide(Side.RIGHT);
yAxis.setLabel(series.getName());
// create chart
LineChart lineChart = new LineChart(xAxis, yAxis);
lineChart.setAnimated(false);
lineChart.setLegendVisible(false);
lineChart.getData().add(series);
styleBackgroundChart(lineChart, lineColor);
setFixedAxisWidth(lineChart);
chartColorMap.put(lineChart, lineColor);
backgroundCharts.add(lineChart);
}
private void styleBackgroundChart(LineChart lineChart, Color lineColor) {
styleChartLine(lineChart, lineColor);
Node contentBackground = lineChart.lookup(".chart-content").lookup(".chart-plot-background");
contentBackground.setStyle("-fx-background-color: transparent;");
lineChart.setVerticalZeroLineVisible(false);
lineChart.setHorizontalZeroLineVisible(false);
lineChart.setVerticalGridLinesVisible(false);
lineChart.setHorizontalGridLinesVisible(false);
lineChart.setCreateSymbols(false);
}
private String toRGBCode(Color color) {
return String.format("#%02X%02X%02X",
(int) (color.getRed() * 255),
(int) (color.getGreen() * 255),
(int) (color.getBlue() * 255));
}
private void styleChartLine(LineChart chart, Color lineColor) {
chart.getYAxis().lookup(".axis-label").setStyle("-fx-text-fill: " + toRGBCode(lineColor) + "; -fx-font-weight: bold;");
Node seriesLine = chart.lookup(".chart-series-line");
seriesLine.setStyle("-fx-stroke: " + toRGBCode(lineColor) + "; -fx-stroke-width: " + strokeWidth + ";");
}
public Node getLegend() {
HBox hBox = new HBox();
final CheckBox baseChartCheckBox = new CheckBox(baseChart.getYAxis().getLabel());
baseChartCheckBox.setSelected(true);
baseChartCheckBox.setStyle("-fx-text-fill: " + toRGBCode(chartColorMap.get(baseChart)) + "; -fx-font-weight: bold;");
baseChartCheckBox.setDisable(true);
baseChartCheckBox.getStyleClass().add("readonly-checkbox");
baseChartCheckBox.setOnAction(event -> baseChartCheckBox.setSelected(true));
hBox.getChildren().add(baseChartCheckBox);
for (final LineChart lineChart : backgroundCharts) {
CheckBox checkBox = new CheckBox(lineChart.getYAxis().getLabel());
checkBox.setStyle("-fx-text-fill: " + toRGBCode(chartColorMap.get(lineChart)) + "; -fx-font-weight: bold");
checkBox.setSelected(true);
checkBox.setOnAction(event -> {
if (backgroundCharts.contains(lineChart)) {
backgroundCharts.remove(lineChart);
} else {
backgroundCharts.add(lineChart);
}
});
hBox.getChildren().add(checkBox);
}
hBox.setAlignment(Pos.CENTER);
hBox.setSpacing(20);
hBox.setStyle("-fx-padding: 0 10 20 10");
return hBox;
}
private class DetailsPopup extends VBox {
private DetailsPopup() {
setStyle("-fx-border-width: 1px; -fx-padding: 5 5 5 5px; -fx-border-color: gray; -fx-background-color: whitesmoke;");
setVisible(false);
}
public void showChartDescrpition(MouseEvent event) {
getChildren().clear();
Long xValueLong = Math.round((double)baseChart.getXAxis().getValueForDisplay(event.getX()));
HBox baseChartPopupRow = buildPopupRow(event, xValueLong, baseChart);
if (baseChartPopupRow != null) {
getChildren().add(baseChartPopupRow);
}
for (LineChart lineChart : backgroundCharts) {
HBox popupRow = buildPopupRow(event, xValueLong, lineChart);
if (popupRow == null) continue;
getChildren().add(popupRow);
}
}
private HBox buildPopupRow(MouseEvent event, Long xValueLong, LineChart lineChart) {
Label seriesName = new Label(lineChart.getYAxis().getLabel());
seriesName.setTextFill(chartColorMap.get(lineChart));
Number yValueForChart = getYValueForX(lineChart, xValueLong.intValue());
if (yValueForChart == null) {
return null;
}
Number yValueLower = Math.round(normalizeYValue(lineChart, event.getY() - 10));
Number yValueUpper = Math.round(normalizeYValue(lineChart, event.getY() + 10));
Number yValueUnderMouse = Math.round((double) lineChart.getYAxis().getValueForDisplay(event.getY()));
// make series name bold when mouse is near given chart's line
if (isMouseNearLine(yValueForChart, yValueUnderMouse, Math.abs(yValueLower.doubleValue()-yValueUpper.doubleValue()))) {
seriesName.setStyle("-fx-font-weight: bold");
}
HBox popupRow = new HBox(10, seriesName, new Label("["+yValueForChart+"]"));
return popupRow;
}
private double normalizeYValue(LineChart lineChart, double value) {
Double val = (Double) lineChart.getYAxis().getValueForDisplay(value);
if (val == null) {
return 0;
} else {
return val;
}
}
private boolean isMouseNearLine(Number realYValue, Number yValueUnderMouse, Double tolerance) {
return (Math.abs(yValueUnderMouse.doubleValue() - realYValue.doubleValue()) < tolerance);
}
public Number getYValueForX(LineChart chart, Number xValue) {
List<XYChart.Data> dataList = ((List<XYChart.Data>)((XYChart.Series)chart.getData().get(0)).getData());
for (XYChart.Data data : dataList) {
if (data.getXValue().equals(xValue)) {
return (Number)data.getYValue();
}
}
return null;
}
}
}
MultipleAxisMainChart:
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.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import java.util.function.Function;
public class MultipleAxesLineChartMain extends Application {
public static final int X_DATA_COUNT = 3600;
#Override
public void start(Stage primaryStage) throws Exception{
NumberAxis xAxis = new NumberAxis(0, X_DATA_COUNT, 200);
NumberAxis yAxis = new NumberAxis();
yAxis.setLabel("Series 1");
LineChart baseChart = new LineChart(xAxis, yAxis);
baseChart.getData().add(prepareSeries("Series 1", (x) -> (double)x));
MultipleAxesLineChart chart = new MultipleAxesLineChart(baseChart, Color.RED);
chart.addSeries(prepareSeries("Series 2", (x) -> (double)x*x),Color.BLUE);
chart.addSeries(prepareSeries("Series 3", (x) -> (double)-x*x),Color.GREEN);
chart.addSeries(prepareSeries("Series 4", (x) -> ((double) (x-250))*x),Color.DARKCYAN);
chart.addSeries(prepareSeries("Series 5", (x) -> ((double)(x+100)*(x-200))),Color.BROWN);
primaryStage.setTitle("MultipleAxesLineChart");
BorderPane borderPane = new BorderPane();
borderPane.setCenter(chart);
borderPane.setBottom(chart.getLegend());
Scene scene = new Scene(borderPane, 1024, 600);
scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
}
private XYChart.Series<Number, Number> prepareSeries(String name, Function<Integer, Double> function) {
XYChart.Series<Number, Number> series = new XYChart.Series<>();
series.setName(name);
for (int i = 0; i < X_DATA_COUNT; i++) {
series.getData().add(new XYChart.Data<>(i, function.apply(i)));
}
return series;
}
public static void main(String[] args) {
launch(args);
}
}
made a little change about the UI, resuting left and right to have same number of yaxis, based on Maciej‘s answer。
preview
My desktop application has a timer for starting and stopping a test. On the graph, I want to create two vertical lines to indicate the start and stop time. "Adding vertical lines to StackPane with JavaFX" won't work for my case because I don't want the lines to stay at the same position and those lines should be drawn within the plot not the layout. When the user zooms on the chart, those vertical lines should move corresponding to where the user zooms. Thanks for any tip.
Here are my codes for creating the chart:
LineChart<Number, Number> chart = new LineChart<Number, Number>(xAxis, yAxis, dataset);
xAxis.setLabel("time(s)");
yAxis.setLabel("deg/s");
You need to extend the LineChart class and override the layoutPlotChildren method in order to show your markers.
Kleopatra did a very good example for a Scatter chart. The code below is a modified version for a line chart and has both vertical and horizontal markers:
public class LineChartSample extends Application {
#Override public void start(Stage stage) {
final NumberAxis xAxis = new NumberAxis();
final NumberAxis yAxis = new NumberAxis();
xAxis.setLabel("Number of Month");
final LineChartWithMarkers<Number,Number> lineChart = new LineChartWithMarkers<Number,Number>(xAxis,yAxis);
XYChart.Series series = new XYChart.Series();
series.setName("My portfolio");
series.getData().add(new XYChart.Data(1, 23));
series.getData().add(new XYChart.Data(2, 14));
series.getData().add(new XYChart.Data(3, 15));
series.getData().add(new XYChart.Data(4, 24));
series.getData().add(new XYChart.Data(5, 34));
series.getData().add(new XYChart.Data(6, 36));
series.getData().add(new XYChart.Data(7, 22));
series.getData().add(new XYChart.Data(8, 45));
series.getData().add(new XYChart.Data(9, 43));
series.getData().add(new XYChart.Data(10, 17));
series.getData().add(new XYChart.Data(11, 29));
series.getData().add(new XYChart.Data(12, 25));
lineChart.getData().add(series);
Data<Number, Number> horizontalMarker = new Data<>(0, 25);
lineChart.addHorizontalValueMarker(horizontalMarker);
Data<Number, Number> verticalMarker = new Data<>(10, 0);
lineChart.addVerticalValueMarker(verticalMarker);
Slider horizontalMarkerSlider = new Slider(yAxis.getLowerBound(), yAxis.getUpperBound(), 0);
horizontalMarkerSlider.setOrientation(Orientation.VERTICAL);
horizontalMarkerSlider.setShowTickLabels(true);
horizontalMarkerSlider.valueProperty().bindBidirectional(horizontalMarker.YValueProperty());
horizontalMarkerSlider.minProperty().bind(yAxis.lowerBoundProperty());
horizontalMarkerSlider.maxProperty().bind(yAxis.upperBoundProperty());
Slider verticalMarkerSlider = new Slider(xAxis.getLowerBound(), xAxis.getUpperBound(), 0);
verticalMarkerSlider.setOrientation(Orientation.HORIZONTAL);
verticalMarkerSlider.setShowTickLabels(true);
verticalMarkerSlider.valueProperty().bindBidirectional(verticalMarker.XValueProperty());
verticalMarkerSlider.minProperty().bind(xAxis.lowerBoundProperty());
verticalMarkerSlider.maxProperty().bind(xAxis.upperBoundProperty());
BorderPane borderPane = new BorderPane();
borderPane.setCenter( lineChart);
borderPane.setTop(verticalMarkerSlider);
borderPane.setRight(horizontalMarkerSlider);
Scene scene = new Scene(borderPane,800,600);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
private class LineChartWithMarkers<X,Y> extends LineChart {
private ObservableList<Data<X, Y>> horizontalMarkers;
private ObservableList<Data<X, Y>> verticalMarkers;
public LineChartWithMarkers(Axis<X> xAxis, Axis<Y> yAxis) {
super(xAxis, yAxis);
horizontalMarkers = FXCollections.observableArrayList(data -> new Observable[] {data.YValueProperty()});
horizontalMarkers.addListener((InvalidationListener)observable -> layoutPlotChildren());
verticalMarkers = FXCollections.observableArrayList(data -> new Observable[] {data.XValueProperty()});
verticalMarkers.addListener((InvalidationListener)observable -> layoutPlotChildren());
}
public void addHorizontalValueMarker(Data<X, Y> marker) {
Objects.requireNonNull(marker, "the marker must not be null");
if (horizontalMarkers.contains(marker)) return;
Line line = new Line();
marker.setNode(line );
getPlotChildren().add(line);
horizontalMarkers.add(marker);
}
public void removeHorizontalValueMarker(Data<X, Y> marker) {
Objects.requireNonNull(marker, "the marker must not be null");
if (marker.getNode() != null) {
getPlotChildren().remove(marker.getNode());
marker.setNode(null);
}
horizontalMarkers.remove(marker);
}
public void addVerticalValueMarker(Data<X, Y> marker) {
Objects.requireNonNull(marker, "the marker must not be null");
if (verticalMarkers.contains(marker)) return;
Line line = new Line();
marker.setNode(line );
getPlotChildren().add(line);
verticalMarkers.add(marker);
}
public void removeVerticalValueMarker(Data<X, Y> marker) {
Objects.requireNonNull(marker, "the marker must not be null");
if (marker.getNode() != null) {
getPlotChildren().remove(marker.getNode());
marker.setNode(null);
}
verticalMarkers.remove(marker);
}
#Override
protected void layoutPlotChildren() {
super.layoutPlotChildren();
for (Data<X, Y> horizontalMarker : horizontalMarkers) {
Line line = (Line) horizontalMarker.getNode();
line.setStartX(0);
line.setEndX(getBoundsInLocal().getWidth());
line.setStartY(getYAxis().getDisplayPosition(horizontalMarker.getYValue()) + 0.5); // 0.5 for crispness
line.setEndY(line.getStartY());
line.toFront();
}
for (Data<X, Y> verticalMarker : verticalMarkers) {
Line line = (Line) verticalMarker.getNode();
line.setStartX(getXAxis().getDisplayPosition(verticalMarker.getXValue()) + 0.5); // 0.5 for crispness
line.setEndX(line.getStartX());
line.setStartY(0d);
line.setEndY(getBoundsInLocal().getHeight());
line.toFront();
}
}
}
}
In order to add more marker lines, just use this:
Data<Number, Number> verticalMarker = new Data<>(10, 0);
lineChart.addVerticalValueMarker(verticalMarker);
Of course you could as well use a rectangle instead of a line like this:
private ObservableList<Data<X, X>> verticalRangeMarkers;
public LineChartWithMarkers(Axis<X> xAxis, Axis<Y> yAxis) {
...
verticalRangeMarkers = FXCollections.observableArrayList(data -> new Observable[] {data.XValueProperty()});
verticalRangeMarkers = FXCollections.observableArrayList(data -> new Observable[] {data.YValueProperty()}); // 2nd type of the range is X type as well
verticalRangeMarkers.addListener((InvalidationListener)observable -> layoutPlotChildren());
}
public void addVerticalRangeMarker(Data<X, X> marker) {
Objects.requireNonNull(marker, "the marker must not be null");
if (verticalRangeMarkers.contains(marker)) return;
Rectangle rectangle = new Rectangle(0,0,0,0);
rectangle.setStroke(Color.TRANSPARENT);
rectangle.setFill(Color.BLUE.deriveColor(1, 1, 1, 0.2));
marker.setNode( rectangle);
getPlotChildren().add(rectangle);
verticalRangeMarkers.add(marker);
}
public void removeVerticalRangeMarker(Data<X, X> marker) {
Objects.requireNonNull(marker, "the marker must not be null");
if (marker.getNode() != null) {
getPlotChildren().remove(marker.getNode());
marker.setNode(null);
}
verticalRangeMarkers.remove(marker);
}
protected void layoutPlotChildren() {
...
for (Data<X, X> verticalRangeMarker : verticalRangeMarkers) {
Rectangle rectangle = (Rectangle) verticalRangeMarker.getNode();
rectangle.setX( getXAxis().getDisplayPosition(verticalRangeMarker.getXValue()) + 0.5); // 0.5 for crispness
rectangle.setWidth( getXAxis().getDisplayPosition(verticalRangeMarker.getYValue()) - getXAxis().getDisplayPosition(verticalRangeMarker.getXValue()));
rectangle.setY(0d);
rectangle.setHeight(getBoundsInLocal().getHeight());
rectangle.toBack();
}
}
used like this:
Data<Number, Number> verticalRangeMarker = new Data<>(4, 10);
lineChart.addVerticalRangeMarker(verticalRangeMarker);
To make it look like a range:
I'm not sure which question you are referring to. You can basically do all this with some binding magic: the trick is to map the x value of the line to coordinates relative to the xAxis using xAxis.getDisplayPosition(...). Then you need to transform that coordinate to the coordinate relative to the container holding the chart and the line: the easiest way to do this is to first transform to Scene coordinates using xAxis.localToScene(...) and then to the coordinates of the container, using container.sceneToLocal(...).
Then you just need to let the binding observe everything that it needs to watch for changes: these will be the (numerical) bounds of the axes, the (graphical) bounds of the chart, and, if the line is going to move, a property representing its x-value.
Here is an SSCCE. In this example, I use a Slider to move the line around. I also make the line visible only if it's in range, and bind the y-coordinates so it spans the yAxis.
import java.util.Random;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ObservableDoubleValue;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
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.control.Slider;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
public class LineChartWithVerticalLine extends Application {
#Override
public void start(Stage primaryStage) {
NumberAxis xAxis = new NumberAxis();
NumberAxis yAxis = new NumberAxis();
LineChart<Number, Number> chart = new LineChart<>(xAxis, yAxis);
chart.getData().add(createSeries());
Pane chartHolder = new Pane();
chartHolder.getChildren().add(chart);
DoubleProperty lineX = new SimpleDoubleProperty();
Slider slider = new Slider();
slider.minProperty().bind(xAxis.lowerBoundProperty());
slider.maxProperty().bind(xAxis.upperBoundProperty());
slider.setPadding(new Insets(20));
lineX.bind(slider.valueProperty());
chartHolder.getChildren().add(createVerticalLine(chart, xAxis, yAxis, chartHolder, lineX));
BorderPane root = new BorderPane(chartHolder, null, null, slider, null);
Scene scene = new Scene(root, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private Line createVerticalLine(XYChart<Number, Number> chart, NumberAxis xAxis, NumberAxis yAxis, Pane container, ObservableDoubleValue x) {
Line line = new Line();
line.startXProperty().bind(Bindings.createDoubleBinding(() -> {
double xInAxis = xAxis.getDisplayPosition(x.get());
Point2D pointInScene = xAxis.localToScene(xInAxis, 0);
double xInContainer = container.sceneToLocal(pointInScene).getX();
return xInContainer ;
},
x,
chart.boundsInParentProperty(),
xAxis.lowerBoundProperty(),
xAxis.upperBoundProperty()));
line.endXProperty().bind(line.startXProperty());
line.startYProperty().bind(Bindings.createDoubleBinding(() -> {
double lowerY = yAxis.getDisplayPosition(yAxis.getLowerBound());
Point2D pointInScene = yAxis.localToScene(0, lowerY);
double yInContainer = container.sceneToLocal(pointInScene).getY();
return yInContainer ;
},
chart.boundsInParentProperty(),
yAxis.lowerBoundProperty()));
line.endYProperty().bind(Bindings.createDoubleBinding(() -> {
double upperY = yAxis.getDisplayPosition(yAxis.getUpperBound());
Point2D pointInScene = yAxis.localToScene(0, upperY);
double yInContainer = container.sceneToLocal(pointInScene).getY();
return yInContainer ;
},
chart.boundsInParentProperty(),
yAxis.lowerBoundProperty()));
line.visibleProperty().bind(
Bindings.lessThan(x, xAxis.lowerBoundProperty())
.and(Bindings.greaterThan(x, xAxis.upperBoundProperty())).not());
return line ;
}
private Series<Number, Number> createSeries() {
Series<Number, Number> series = new Series<>();
series.setName("Data");
Random rng = new Random();
for (int i=0; i<=20; i++) {
series.getData().add(new Data<>(i, rng.nextInt(101)));
}
return series ;
}
public static void main(String[] args) {
launch(args);
}
}
I was able to create a drag and zoom in feature using the Line Chart Example mentioned here. The code listens to the mouse events and adds to the vertical ranges, which makes it appear to be dragging. JavaFX Drag and Zoom Line Chart Example
/**
* The ChartView.
*/
public class ChartController {
private ChartViewModel chartViewModel;
private CustomLineChart<Number, Number> lineChart;
private NumberAxis xAxis;
private NumberAxis yAxis;
private XYChart.Series<Number, Number> series;
private List<Integer> data;
private boolean mouseDragged;
private double initialNumberStart;
private double initialNumberEnd;
#FXML
private VBox mainContainer;
#FXML
private HBox chartContainer;
/**
* The constructor.
*/
public ChartController() {
chartViewModel = new ChartViewModel();
mouseDragged = false;
}
/**
* The initialize method.
*/
public void initialize() {
createChart();
handleEvents();
}
/**
* Handles the events.
*/
private void handleEvents() {
lineChart.setOnMousePressed(pressed -> {
int minSize = 1;
// Get coordinate from the scene and transform to coordinates from the chart axis
Point2D firstSceneCoordinate = new Point2D(pressed.getSceneX(), pressed.getSceneY());
double firstX = xAxis.sceneToLocal(firstSceneCoordinate).getX();
lineChart.setOnMouseDragged(dragged -> {
mouseDragged = true;
Point2D draggedSceneCoordinate = new Point2D(dragged.getSceneX(), dragged.getSceneY());
double draggedX = xAxis.sceneToLocal(draggedSceneCoordinate).getX();
List<Double> numbers = filterSeries(firstX, draggedX);
int size = numbers.size();
double numberStart = size > minSize ? numbers.get(0) : initialNumberStart;
double numberEnd = numbers.size() > minSize ? numbers.get(size - 1) : initialNumberEnd;
if (size > minSize) {
lineChart.addVerticalRangeLines(new Data<>(numberStart, numberEnd));
}
lineChart.setOnMouseReleased(released -> {
if (mouseDragged) {
initialNumberStart = numberStart;
initialNumberEnd = numberEnd;
mouseDragged = false;
redrawChart();
}
});
});
});
}
/**
* Creates the charts.
*/
private void createChart() {
xAxis = new NumberAxis();
yAxis = new NumberAxis();
lineChart = new CustomLineChart<>(xAxis, yAxis);
data = chartViewModel.getData();
createSeries(data);
lineChart.getData().add(series);
initialNumberStart = 1;
initialNumberEnd = data.size() - 1;
chartContainer.getChildren().add(lineChart);
HBox.setHgrow(lineChart, Priority.ALWAYS);
}
/**
* Creates the series for the line chart.
*
* #param numbers The list of numbers for the series
*/
private void createSeries(List<Integer> numbers) {
int size = numbers.size();
series = new XYChart.Series<>();
series.setName("Example");
for (int i = 0; i < size; i++) {
series.getData().add(new XYChart.Data<Number, Number>(i, numbers.get(i)));
}
}
/**
* Filters the nodes and returns the node x positions within the firstX and lastX positions.
*
* #param firstX The first x position
* #param lastX The last x position
* #return The x positions for the nodes within the firstX and lastX
*/
private List<Double> filterSeries(double firstX, double lastX) {
List<Double> nodeXPositions = new ArrayList<>();
lineChart.getData().get(0).getData().forEach(node -> {
double nodeXPosition = lineChart.getXAxis().getDisplayPosition(node.getXValue());
if (nodeXPosition >= firstX && nodeXPosition <= lastX) {
nodeXPositions.add(Double.parseDouble(node.getXValue().toString()));
}
});
return nodeXPositions;
}
/**
* Updates the series for the chart.
*/
private void updateSeries() {
lineChart.getData().remove(0);
lineChart.getData().add(series);
}
/**
* Redraws the chart.
*/
private void redrawChart() {
List<Integer> filteredSeries = new ArrayList<>();
data.forEach(number -> {
if (number >= initialNumberStart && number <= initialNumberEnd) {
filteredSeries.add(number);
}
});
if (!filteredSeries.isEmpty()) {
createSeries(filteredSeries);
updateSeries();
lineChart.removeVerticalRangeLines();
}
}
/**
* Resets the series for the chart.
*
* #param event The event
*/
#FXML
void resetChart(ActionEvent event) {
createSeries(data);
updateSeries();
}
}