Skip to content

Commit

Permalink
Financial Chart: CandleStickRenderer, HighLowRenderer, Basic Financia…
Browse files Browse the repository at this point in the history
…l API (#316)

Financial Chart - CandleStickRenderer, HighLowRenderer, Basic Financial API
  • Loading branch information
raven2cz committed Nov 24, 2020
1 parent 8caa280 commit a36e779
Show file tree
Hide file tree
Showing 57 changed files with 8,633 additions and 15 deletions.
32 changes: 27 additions & 5 deletions README.md
Expand Up @@ -11,7 +11,7 @@
# ChartFx

ChartFx is a scientific charting library developed at [GSI](https://www.gsi.de) for FAIR with focus on performance optimised real-time data visualisation at 25 Hz update rates for data sets with a few 10 thousand up to 5 million data points common in digital signal processing applications.
Based on earlier Swing-based designs used at GSI and CERN, it is a re-write of JavaFX's default [Chart](https://docs.oracle.com/javase/8/javafx/api/javafx/scene/chart/Chart.html) implementation and aims to preserve the feature-rich and extensible functionality of earlier and other similar Swing-based libraries while addressing the performance bottlenecks and API issues.
Based on earlier Swing-based designs used at GSI and CERN, it is a re-write of JavaFX's default [Chart](https://docs.oracle.com/javase/8/javafx/api/javafx/scene/chart/Chart.html) implementation and aims to preserve the feature-rich and extensible functionality of earlier and other similar Swing-based libraries while addressing the performance bottlenecks and API issues.
The motivation for the re-design has been presented at [IPAC'19](https://ipac19.org/) ([paper](docs/THPRB028.pdf), [poster](docs/THPRB028_poster.pdf)).

<figure>
Expand Down Expand Up @@ -234,6 +234,28 @@ mvn exec:java

</table>

### Financial related examples
Financial charts are types of charts that visually track various business and financial metrics like liquidity, price movement, expenses, cash flow, and others over a given a period of the time. Financial charts are a great way to express a story about business or financial markets (instruments, financial assets).

The chart-fx samples submodule contains financial charts and toolbox samples.

If you want to try them yourself run:

```bash
mvn compile install
mvn exec:java
```

<table>
<tr>
<td><figure><img src="docs/pics/FinancialCandlestickSample.png" alt="FinancialCandlestickSample" width=600/><figcaption><a href="chartfx-samples/src/main/java/de/gsi/chart/samples/financial/FinancialCandlestickSample.java">FinancialCandlestickSample.java</a></figcaption></figure></td>
<td><figure><img src="docs/pics/FinancialHiLowSample.png" alt="FinancialHiLowSample" width=600/><figcaption><a href="chartfx-samples/src/main/java/de/gsi/chart/samples/financial/FinancialHiLowSample.java">FinancialHiLowSample.java</a></figcaption></figure></td>
</tr>
<tr>
<td><figure><img src="docs/pics/FinancialAdvancedCandlestickSample.png" alt="FinancialAdvancedCandlestickSample" width=600/><figcaption><a href="chartfx-samples/src/main/java/de/gsi/chart/samples/financial/FinancialAdvancedCandlestickSample.java">FinancialAdvancedCandlestickSample.java</a></figcaption></figure></td>
</tr>
</table>

### Math- & Signal-Processing related examples
The math samples can be started by running:

Expand Down Expand Up @@ -288,7 +310,7 @@ Besides the extended functionality outlined above, the ChartFx optimisation goal
<img src="docs/pics/chartfx-performance1a.png" alt="JavaFX-ChartFx performance comparison for 2 Hz" width=90%/>
<figcaption>Performance comparison @ 2 Hz update rate.</figcaption>
</figure></td></tr>
</table>
</table>

While the ChartFx implementation already achieved a better functionality and a by two orders of magnitude improved performance for very large datasets, the basic test scenario has also been checked against popular existing Java-Swing and non-Java based UI charting frameworks. The Figure below provides a summary of the evaluated chart libraries for update rates at 25 Hz and 1k samples.

Expand All @@ -300,8 +322,8 @@ While the ChartFx implementation already achieved a better functionality and a b
</figure>

## Some thoughts
While starting out to improve the JDK's JavaFX Chart functionality and performance through initially extending, then gradually replacing bottle-necks, and eventually re-designing and replacing the original implementations, the resulting ChartFx library provides a substantially larger functionality and achieved an about two orders of magnitude performance improvement.
Nevertheless, improved functionality aside, a direct performance comparison even for the best-case JavaFX scenario (static axes) with other non-JavaFX libraries demonstrated the raw JavaFX graphics performance -- despite the redesign -- being still behind the existing Java Swing-based JDataViewer and most noticeable the Qt Charts implementations. The library will continued to be maintained here at GitHub and further used for existing and future JavaFX-based control room UIs at GSI.
While starting out to improve the JDK's JavaFX Chart functionality and performance through initially extending, then gradually replacing bottle-necks, and eventually re-designing and replacing the original implementations, the resulting ChartFx library provides a substantially larger functionality and achieved an about two orders of magnitude performance improvement.
Nevertheless, improved functionality aside, a direct performance comparison even for the best-case JavaFX scenario (static axes) with other non-JavaFX libraries demonstrated the raw JavaFX graphics performance -- despite the redesign -- being still behind the existing Java Swing-based JDataViewer and most noticeable the Qt Charts implementations. The library will continued to be maintained here at GitHub and further used for existing and future JavaFX-based control room UIs at GSI.
The gained experience and interfaces will provide a starting point for a planned C++-based counter-part implementation using Qt or another suitable low-level charting library.

## Working on the source
Expand Down Expand Up @@ -354,7 +376,7 @@ The Plugin Base class provides you with access to the chart object using `getCha
Your plugin should always add a Listener to the chartProperty, because when it is created there will not be an associated
chart, so at creation time, calls to e.g. `getChart()` will return null.
Using a custom plugin boils down to adding it to the chart by doing `chart.getPlugins().add(new MyPlugin())`.
If you wrote a plugin which might be useful for other users of chart-fx please consider doing a pull request against chart-fx.
If you wrote a plugin which might be useful for other users of chart-fx please consider doing a pull request against chart-fx.

Renderers are the components which do the actual heavy lifting in drawing the components of the graph to the canvas.
A chart can have multiple renderers added using `chart.getRenderers().add(...)`
Expand Down
5 changes: 5 additions & 0 deletions chartfx-chart/src/main/java/de/gsi/chart/Chart.java
Expand Up @@ -27,6 +27,7 @@
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import javafx.scene.paint.Paint;
import javafx.stage.Window;
import javafx.util.Duration;

Expand Down Expand Up @@ -843,6 +844,10 @@ public final void setTitleSide(final Side value) {
titleSide.set(value);
}

public final void setTitlePaint(final Paint paint) {
titleLabel.setTextFill(paint);
}

public Chart setToolBarPinned(boolean value) {
toolBarPinned.set(value);
return this;
Expand Down
@@ -0,0 +1,138 @@
package de.gsi.chart.renderer.spi.financial;

import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;

import de.gsi.chart.axes.Axis;
import de.gsi.chart.renderer.Renderer;
import de.gsi.chart.renderer.spi.AbstractDataSetManagement;
import de.gsi.chart.renderer.spi.financial.service.OhlcvRendererEpData;
import de.gsi.chart.renderer.spi.financial.service.PaintBarMarker;
import de.gsi.dataset.DataSet;
import de.gsi.dataset.spi.financial.OhlcvDataSet;

/**
* The ancestor for common financial renderers.
* If you use this parent for your financial renderers you can use general features:
* <ul>
* <li>PaintBar - coloring and painting changes of specific bars/candles/lines/dots</li>
* <li>Shadows - specific fast shadow paintings without fx-effects</li>
* <li>Extension-point before/after painting - extend specific renderers by your changes to add EP rules.</li>
* </ul>
*/
@SuppressWarnings({ "PMD.ExcessiveParameterList" })
public abstract class AbstractFinancialRenderer<R extends Renderer> extends AbstractDataSetManagement<R> implements Renderer {
protected PaintBarMarker paintBarMarker;

/**
* Inject PaintBar Marker service
*
* @param paintBarMarker service implementation
*/
public void setPaintBarMarker(PaintBarMarker paintBarMarker) {
this.paintBarMarker = paintBarMarker;
}

/**
* Simple algorithmic solution to calculate required chart area distances.
*
* @param findAreaDistances service for calculation of find chart area distances.
* @param dataset includes data for processing
* @param xAxis X-Axis DO
* @param yAxis Y-Axis DO
* @param xmin minimal value of X
* @param xmax maximal value of X
* @return the calculated distances
*/
protected double[] findAreaDistances(FindAreaDistances findAreaDistances,
DataSet dataset, Axis xAxis, Axis yAxis, double xmin, double xmax) {
return findAreaDistances.findAreaDistances(dataset, xAxis, yAxis, xmin, xmax);
}

/**
* Specific painting/coloring of the OHLCV/Candle Bars.
* If you need specific bar selection visualization - implement this service and write your selection.
*
* @param data domain object for Renderer Extension Points
* @return the specific paint bar Paint
*/
protected Paint getPaintBarColor(OhlcvRendererEpData data) {
if (paintBarMarker != null) {
return paintBarMarker.getPaintBy(data);
}
return null;
}

/**
* Possibility paint volume to financial renderers
*
* @param gc GraphicsContext
* @param ds DataSet domain object which contains volume data
* @param index actual index which is rendered
* @param volumeLongColor volume color for Long Uptick OHLC
* @param volumeShortColor volume color for Short Uptick OHLC
* @param yAxis Y-Axis DO
* @param distances distances estimated from finding service
* @param barWidth width of bar
* @param barWidthHalf half width of bar
* @param x0 the center of the bar for X coordination
*/
protected void paintVolume(GraphicsContext gc, DataSet ds, int index, Color volumeLongColor, Color volumeShortColor, Axis yAxis, double[] distances, double barWidth,
double barWidthHalf, double x0) {
double volume = ds.get(OhlcvDataSet.DIM_Y_VOLUME, index);
double open = ds.get(OhlcvDataSet.DIM_Y_OPEN, index);
double close = ds.get(OhlcvDataSet.DIM_Y_CLOSE, index);
double maxVolume = distances[1];
double volumeHeight = (volume / maxVolume) * 0.3;
double min = yAxis.getDisplayPosition(yAxis.getMin());
double max = yAxis.getDisplayPosition(yAxis.getMax());
double zzVolume = volumeHeight * (max - min);

gc.setFill(open < close ? volumeLongColor : volumeShortColor);
gc.fillRect(x0 - barWidthHalf, min + zzVolume, barWidth, -zzVolume);
}

// services --------------------------------------------------------

@FunctionalInterface
protected interface FindAreaDistances {
double[] findAreaDistances(DataSet dataset, Axis xAxis, Axis yAxis, double xmin, double xmax);
}

protected static class XMinAreaDistances implements FindAreaDistances {
@Override
public double[] findAreaDistances(DataSet dataset, Axis xAxis, Axis yAxis, double xmin, double xmax) {
double minDistance = Integer.MAX_VALUE;
for (int i = dataset.getIndex(DataSet.DIM_X, xmin) + 1; i < Math.min(dataset.getIndex(DataSet.DIM_X, xmax) + 1, dataset.getDataCount()); i++) {
final double param0 = xAxis.getDisplayPosition(dataset.get(DataSet.DIM_X, i - 1));
final double param1 = xAxis.getDisplayPosition(dataset.get(DataSet.DIM_X, i));

if (param0 != param1) {
minDistance = Math.min(minDistance, Math.abs(param1 - param0));
}
}
return new double[] { minDistance };
}
}

protected static class XMinVolumeMaxAreaDistances implements FindAreaDistances {
@Override
public double[] findAreaDistances(DataSet dataset, Axis xAxis, Axis yAxis, double xmin, double xmax) {
double minDistance = Integer.MAX_VALUE;
double maxVolume = Integer.MIN_VALUE;
for (int i = dataset.getIndex(DataSet.DIM_X, xmin) + 1; i < Math.min(dataset.getIndex(DataSet.DIM_X, xmax) + 1, dataset.getDataCount()); i++) {
final double param0 = xAxis.getDisplayPosition(dataset.get(DataSet.DIM_X, i - 1));
final double param1 = xAxis.getDisplayPosition(dataset.get(DataSet.DIM_X, i));
double volume = dataset.get(OhlcvDataSet.DIM_Y_VOLUME, i);
if (maxVolume < volume) {
maxVolume = volume;
}
if (param0 != param1) {
minDistance = Math.min(minDistance, Math.abs(param1 - param0));
}
}
return new double[] { minDistance, maxVolume };
}
}
}

0 comments on commit a36e779

Please sign in to comment.