Skip to content

extjfx/extjfx

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

32 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Build Status License

ExtJFX

ExtJFX is a small library developed at CERN containing features needed by our JavaFX applications that are not supported by the standard JavaFX toolkit. The library consists of 3 modules:

  • extjfx-chart: zooming, panning, data annotations, value/range indicators, chart decorations, overlaying different types of charts, etc.
  • extjfx-fxml: FxmlView class that simplifies loading FXML files using conventional names
  • extjfx-test: FxJUnit4Runner to execute GUI tests
  • extjfx-samples: Executable jar with chart samples

Build Artifacts

extjfx-chart

XYChartPane

The central class of the cern.extjfx.chart package is XYChartPane. It is a container that can hold one or more instances of XYChart (e.g. LineChart, AreaChart, BarChart). XYChartPane brings support for overlaying different chart types on top of each other (as on the figure below) and possibility to add chart plugins (instances of XYChartPlugin) which can be either interacting components e.g. Zoomer or Panner, or passive graphical elements drawn on the chart such as labels or data indicators.

An example of overlay charts: AreaChart, LineChart and ScatterChart with independent axes for each chart

The corresponding source code (expand)
public class MixedChartSample extends Application {
    private static final List<String> DAYS = new ArrayList<>(
            Arrays.asList("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"));

    @Override
    public void start(Stage stage) throws Exception {
        stage.setTitle("Mixed Chart Sample");

        BarChart<String, Number> barChart = new BarChart<>(createXAxis(), createYAxis());
        barChart.getStyleClass().add("chart1");
        barChart.setAnimated(false);
        barChart.getYAxis().setLabel("Data 1");
        barChart.getYAxis().setSide(Side.LEFT);
        barChart.getData().add(new Series<>("Data 1", createTestData(3)));

        LineChart<String, Number> lineChart = new LineChart<>(createXAxis(), createYAxis());
        lineChart.getStyleClass().add("chart2");
        lineChart.setAnimated(false);
        lineChart.setCreateSymbols(true);
        lineChart.getYAxis().setLabel("Data 2");
        lineChart.getYAxis().setSide(Side.RIGHT);
        lineChart.getData().add(new Series<>("Data 2", createTestData(10)));
        
        ScatterChart<String, Number> scatterChart = new ScatterChart<>(createXAxis(), createYAxis());
        scatterChart.getStyleClass().add("chart3");
        scatterChart.setAnimated(false);
        scatterChart.getYAxis().setLabel("Data 3");
        scatterChart.getYAxis().setSide(Side.RIGHT);
        scatterChart.getData().add(new Series<>("Data 3", createTestData(20)));

        XYChartPane<String, Number> chartPane = new XYChartPane<>(barChart);
        chartPane.setTitle("Mixed chart types");
        chartPane.setCommonYAxis(false);
        chartPane.getOverlayCharts().addAll(lineChart, scatterChart);
        chartPane.getPlugins().addAll(new CrosshairIndicator<>(), new DataPointTooltip<>());
        chartPane.getStylesheets().add("mixed-chart-sample.css");

        BorderPane borderPane = new BorderPane(chartPane);
        Scene scene = new Scene(borderPane, 800, 600);
        stage.setScene(scene);
        stage.show();
    }

    private NumericAxis createYAxis() {
        NumericAxis yAxis = new NumericAxis();
        yAxis.setAnimated(false);
        yAxis.setForceZeroInRange(false);
        yAxis.setAutoRangePadding(0.1);
        yAxis.setAutoRangeRounding(true);
        return yAxis;
    }

    private CategoryAxis createXAxis() {
        CategoryAxis xAxis = new CategoryAxis();
        xAxis.setAnimated(false);
        return xAxis;
    }

    private ObservableList<Data<String, Number>> createTestData(double refVal) {
        Random rnd = new Random();
        List<Data<String, Number>> data = new ArrayList<>();
        for (int i = 0; i < DAYS.size(); i++) {
            data.add(new Data<>(DAYS.get(i), refVal - Math.abs(3 - i) + rnd.nextDouble()));
        }
        return FXCollections.observableArrayList(data);
    }    
    
    public static void main(String[] args) {
        launch(args);
    }
}
The corresponding CSS (expand)
.chart1 .chart-bar { -fx-bar-fill: #22bad9; }
.chart1 .axis:left { -fx-tick-label-fill: #22bad9; }
.chart1 .axis:left .axis-label { -fx-text-fill: #22bad9; }

.chart2 .axis:right { -fx-tick-label-fill: #c62b00; }
.chart2 .axis:right .axis-label { -fx-text-fill: #c62b00; }
.chart2 .chart-series-line { -fx-stroke: #c62b00; }
.chart2 .chart-line-symbol { -fx-background-color: #c62b00, white; }

.chart3 .axis:right { -fx-tick-label-fill: green; }
.chart3 .axis:right .axis-label { -fx-text-fill: green; }
.chart3 .chart-symbol { 
    -fx-background-color: green;
    -fx-background-radius: 0;
    -fx-padding: 7px 5px 7px 5px;
    -fx-shape: "M5,0 L10,9 L5,18 L0,9 Z";
}

The XYChartPane allows having a single (shared) Y axis or distinct axes, one per overlaid chart.

Note that in order to draw charts properly on top of each other some properties of the overlaid charts are overridden - see JavaDoc of XYChartPane for details.

Chart Plugins

Chart plugins are add-ons to the standard charts that can be added to the XYChartPane to either interact with chart content or to decorate it. Currently the package provides the following plugins:

  • ChartOverlay allows adding any node on top of the chart area.
  • CrosshairIndicator a cross (horizontal and vertical line) following mouse cursor and displaying current coordinates
  • DataPointTooltip a tooltip label displaying coordinates of the data point hovered by the mouse cursor
  • Zoomer zooms the plot area to the dragged rectangle
  • Panner allows dragging the visible data window with mouse cursor
  • XValueIndicator and YValueIndicator a vertical or horizontal line (accordingly) indicating specified X or Y value, with optional text label that can be used to describe the indicated value
  • XRangeIndicator and YRangeIndicator a rectangle indicating vertical or horizontal range (accordingly) of X or Y values, with optional text label that can be used to describe the indicated range

The following example presents all plugins on a single chart pane.

Source code (expand)
public class PluginsSample extends Application {

  @Override
    public void start(Stage stage) {
        stage.setTitle("Plugins Sample");
        
       NumericAxis xAxis = new NumericAxis();
        xAxis.setLabel("X Values");
         
        NumericAxis yAxis = new NumericAxis();
        yAxis.setAutoRangePadding(0.1);
        yAxis.setLabel("Y Values");
         
        LineChart<Number, Number> chart = new LineChart<>(xAxis, yAxis);
        chart.getData().add(new Series<>("Test Data", createTestData()));
         
        XYChartPane<Number, Number> chartPane = new XYChartPane<>(chart);
        XValueIndicator<Number> internalStop = new XValueIndicator<>(75, "Internal Stop");
        internalStop.setLabelPosition(0.95);
        chartPane.getPlugins().add(internalStop);
         
        YValueIndicator<Number> yMin = new YValueIndicator<>(-7.5, "MIN");
        yMin.setLabelPosition(0.1);
        YValueIndicator<Number> yMax = new YValueIndicator<>(7.5, "MAX");
        yMax.setLabelPosition(0.1);
        chartPane.getPlugins().addAll(yMin, yMax);
        XRangeIndicator<Number> xRange = new XRangeIndicator<>(40, 60, "X Range");
        xRange.setLabelVerticalPosition(0.95);
        chartPane.getPlugins().add(xRange);
         
        YRangeIndicator<Number> thresholds = new YRangeIndicator<>(-5, 5, "Thresholds");
        thresholds.setLabelHorizontalAnchor(HPos.RIGHT);
        thresholds.setLabelHorizontalPosition(0.95);
        thresholds.setLabelVerticalAnchor(VPos.TOP);
        thresholds.setLabelVerticalPosition(0.95);
        chartPane.getPlugins().add(thresholds);
         
        Label label = new Label("Label added to the chart pane\n using ChartOverlay");
        label.setStyle("-fx-background-color: rgba(255, 127, 80, 0.5)");
        AnchorPane.setLeftAnchor(label, 5.0);
        AnchorPane.setTopAnchor(label, 5.0);
        chartPane.getPlugins().add(new ChartOverlay<>(OverlayArea.PLOT_AREA, new AnchorPane(label)));
        chartPane.getPlugins().addAll(new Zoomer(), new Panner(), new CrosshairIndicator<>(), new DataPointTooltip<>());
        chartPane.getStylesheets().add(getClass().getResource("plugins-sample.css").toExternalForm());
  
        Scene scene = new Scene(chartPane, 800, 600);
        stage.setScene(scene);
        stage.show();
    }
    
    private ObservableList<Data<Number, Number>> createTestData() {
        Random rnd = new Random(System.currentTimeMillis());
        List<Data<Number, Number>> data = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            data.add(new Data<>(i, (rnd.nextBoolean() ? 1 : -1) * 10 * rnd.nextDouble()));
        }
        return FXCollections.observableArrayList(data);
    }
    
    public static void main(String[] args) {
        launch(args);
    }
 }
The associated CSS file (expand)
.x-value-indicator-label { 
	-fx-background-color: pink; 
}
.x-value-indicator-line  {
    -fx-stroke: pink;
    -fx-stroke-width: 2;
}
.x-range-indicator-rect {
    -fx-fill: rgba(173, 255, 47, 0.5);
}
 
.y-range-indicator-label {
    -fx-background-color: orange;
}
.y-range-indicator-rect {
    -fx-stroke: orange;
    -fx-fill: #416ef468;
}
.y-value-indicator-label {
    -fx-background-color: red;
}
.y-value-indicator-line {
    -fx-stroke: red;
}

The resulting chart:

Plugins Example

NumericAxis and LogarithmicAxis

Note that in the examples above we used cern.extjfx.chart.NumericAxis rather than javafx.scene.chart.NumberAxis which does not support proper recalculation of tick units with auto-range being switched off (necessary behavior for Zoomer and Panner to work properly).

In addition to the NumericAxis the package contains also LogarithmicAxis with a configurable logarithm base (by default 10):

NumericAxis xAxis = new NumericAxis();
LogarithmicAxis yAxis = new LogarithmicAxis();
LineChart<Number, Number> lineChart = new LineChart<>(xAxis, yAxis);
lineChart.setTitle("Test data");
...

Logarithmic Axis

HeatMapChart

HeatMapChart is a specialized chart that uses colors to represent data values. The following figure presents a particle beam image rendered using HeatMapChart.

Beam Image

Source code (expand)
NumericAxis xAxis = new NumericAxis();
xAxis.setAnimated(false);
xAxis.setAutoRangeRounding(false);
xAxis.setLabel("X Position");
 
NumericAxis yAxis = new NumericAxis();
yAxis.setAnimated(false);
yAxis.setAutoRangeRounding(false);
yAxis.setLabel("Y Position");
 
HeatMapChart<Number, Number> chart = new HeatMapChart<>(xAxis, yAxis);
chart.setTitle("Beam Image");
 
// readImage() creates a DefaultData class containing X, Y and Z values
chart.setData(readImage());
chart.setLegendVisible(true);
chart.setLegendSide(Side.RIGHT);

By default the HeatMapChart uses a rainbow colors gradient but this can be changed using colorGradient property (see JavaDoc for details).

The chart can be also used in combination with JavaFX CategoryAxis:

HeatMapChart with CategoryAxis

Source code (expand)
@Override
public void start(Stage primaryStage) {
    primaryStage.setTitle("HeatMapChart Category Sample");
    
    CategoryAxis xAxis = new CategoryAxis();
    xAxis.setLabel("Week Days");
    CategoryAxis yAxis = new CategoryAxis();
    yAxis.setLabel("Teams");
    
    HeatMapChart<String, String> chart = new HeatMapChart<>(xAxis, yAxis);
    chart.setTitle("Avg #coffees per Person");
    chart.setColorGradient(ColorGradient.BLUE_RED);
    chart.setData(createData());
    chart.setLegendVisible(true);
    chart.setLegendSide(Side.RIGHT);
     
    Scene scene = new Scene(chart, 800, 600);
    primaryStage.setScene(scene);
    primaryStage.show();
}
 
private static Data<String, String> createData() {
    String[] team = {"A", "B", "C", "D", "E"};
    String[] days = {"Mon", "Tue", "Wed", "Thu", "Fri"};
     
    Random rnd = new Random();
    double[][] coffees = new double[days.length][team.length];
    for (int i = 0; i < days.length; i++) {
        for (int j = 0; j < team.length; j++) {
            coffees[i][j] = 3 * rnd.nextDouble();
        }
    }
    return new DefaultData<>(days, team, coffees);
}

Dealing With Large Data Sets

The JavaFX charting package performs well with series containing up to a few thousands data points, with rendering time below one second (on a decent desktop computer). However, drawing series containing tens of thousands points takes several seconds, blocking the FX thread and making the application unresponsive.

To overcome the performance issues, the extjfx-chart package provides DataReducingObservableList that performs data reduction to the specified number of points within the given X data range. i.e. it reduces only the part of initial data set that is currently visible on the chart. This means that while performing a zoom-in, one can see "more details" in the interesting region.

By default DataReducingObservableList uses DefaultDataReducer that is an implementation of Ramer-Douglas-Peucker algorithm - sufficiently fast and giving desired results in majority of cases. If zooming-in is not needed, the DefaultDataReducer can be used directly to filter the data, before it is passed to the chart. As the alternative, you can also use LinearDataReducer.

Example source code (expand)
NumericAxis xAxis = new NumericAxis();
xAxis.setAnimated(false);
NumericAxis yAxis = new NumericAxis();
yAxis.setAnimated(false);
LineChart<Number, Number> lineChart = new LineChart<>(xAxis, yAxis);
lineChart.setTitle("Test data");
 
DataReducingObservableList<Number, Number> reducedData = new DataReducingObservableList<>(xAxis);
lineChart.getData().add(new Series<>("Random data", reducedData));
 
ArrayData<Number, Number> sourceData =  ArrayData.of(RandomDataGenerator.generateArrayData(0, 1, MAX_NUMBER_OF_POINTS, 0)); 
reducedData.setData(sourceData);

extjfx-fxml

The package contains the FxmlView class that for a that for a given controller (class or instance) loads corresponding FXML file and applies associated CSS file (if present). The FXML and CSS files are searched in the same package as controller's class and are expected to have the same conventional name i.e. for MainController, they should be called Main.fxml and Main.css respectively.

The class supports also loading corresponding resource bundle file (if present) that is expected to follow the same naming convention e.g.Main_en_US.properties. The package structure would look in the following way:

com.mycompany.myapp.MainController.java
com.mycompany.myapp.Main.fxml
com.mycompany.myapp.Main.css
com.mycompany.myapp.Main_en_US.properties

Example usage:

FxmlView mainView = new FxmlView(MainController.class);
Scene scene = new Scene(mainView.getRootNode(), 400, 400);
// ...
MainController mainController = mainView.getController();
mainController.doSomething();

Controllers are instantiated using configured controller factory that by default is initialized to DefaultControllerFactory.createController(Class) which supports basic Dependency Injection (see JavaDoc for details).

The FxmlView class was inspired by Adam Bien's FXMLView from the afterburner.fx framework, with the difference that it doesn't require a separate view class per FXML.

extjfx-test

Contains FxJUnit4Runner - a JUnit runner to execute JavaFX tests:

@RunWith(FxJUnit4Runner.class)
public class MyControlTest {

    @Test
    public void testInJUnitThread() {
        // Do testing
    }

    @Test
    @RunInFxThread
    public void testInJavaFXThread() {
        // Do testing
    }
}

extjfx-samples

Sample application with chart examples: zooming, panning, decorations, HeatMapChart, etc. Download the executable jar with samples and run:

java -jar extjfx-samples-[version].jar