Skip to main content
ta4j includes a built-in charting layer on top of JFreeChart. The ChartWorkflow builder API assembles price data, indicator overlays, trading signals, and performance subcharts into a single chart with a consistent visual style.
The charting API lives in the ta4j-examples module. Add ta4j-examples as a dependency to use it.

Basic strategy chart

The simplest chart shows price bars, indicator overlays, and buy/sell signals.
import ta4jexamples.charting.workflow.ChartWorkflow;
import ta4jexamples.charting.builder.TimeAxisMode;
import org.jfree.chart.JFreeChart;

// EMA crossover strategy setup
ClosePriceIndicator close = new ClosePriceIndicator(series);
EMAIndicator fastEma = new EMAIndicator(close, 12);
EMAIndicator slowEma = new EMAIndicator(close, 26);
Rule entry = new CrossedUpIndicatorRule(fastEma, slowEma);
Rule exit  = new CrossedDownIndicatorRule(fastEma, slowEma);
Strategy strategy = new BaseStrategy("EMA Crossover", entry, exit);
TradingRecord record = new BarSeriesManager(series).run(strategy);

ChartWorkflow chartWorkflow = new ChartWorkflow();
JFreeChart chart = chartWorkflow.builder()
        .withTitle("EMA Crossover Strategy")
        .withTimeAxisMode(TimeAxisMode.BAR_INDEX) // compress weekend/holiday gaps
        .withSeries(series)                        // candlestick price data
        .withIndicatorOverlay(fastEma)             // overlay on price chart
        .withIndicatorOverlay(slowEma)
        .withTradingRecordOverlay(record)          // entry/exit arrows
        .toChart();

// Save as image
chartWorkflow.saveChartImage(chart, series, "ema-crossover-strategy", "output/charts");

Builder API reference

withSeries(series)

Adds OHLCV candlestick price data as the main price panel. This is the base layer every chart needs.

withIndicatorOverlay(indicator)

Draws an indicator as a line on the main price panel. Use this for indicators that operate on the same scale as price (moving averages, Bollinger Bands, VWAP, etc.).

withTradingRecordOverlay(record)

Marks entry and exit points on the price panel with directional arrows. Entry signals point up; exit signals point down.

withSubChart(indicator)

Adds an indicator in a separate panel below the price chart. Use this for indicators with a different scale than price, such as RSI (0–100), MACD, or volume.
ChartWorkflow chartWorkflow = new ChartWorkflow();
JFreeChart chart = chartWorkflow.builder()
        .withTitle("RSI Strategy with Subchart")
        .withSeries(series)
        .withTradingRecordOverlay(record)
        .withSubChart(rsi)   // RSI in its own panel, separate scale
        .toChart();

withSubChart(criterion, record)

Adds a performance criterion as a subchart. The criterion is evaluated bar by bar across the trading record, giving you a time-series view of metrics like maximum drawdown or net profit.
import org.ta4j.core.criteria.drawdown.MaximumDrawdownCriterion;

JFreeChart chart = chartWorkflow.builder()
        .withTitle("Strategy Performance Analysis")
        .withSeries(series)
        .withIndicatorOverlay(sma20)
        .withIndicatorOverlay(ema12)
        .withTradingRecordOverlay(record)
        .withSubChart(new MaximumDrawdownCriterion(), record)
        .toChart();

Indicator subchart example (RSI)

ClosePriceIndicator close = new ClosePriceIndicator(series);
RSIIndicator rsi = new RSIIndicator(close, 14);

Rule entry = new CrossedDownIndicatorRule(rsi, 30); // oversold
Rule exit  = new CrossedUpIndicatorRule(rsi, 70);   // overbought
Strategy strategy = new BaseStrategy("RSI Strategy", entry, exit);
TradingRecord record = new BarSeriesManager(series).run(strategy);

ChartWorkflow chartWorkflow = new ChartWorkflow();
JFreeChart chart = chartWorkflow.builder()
        .withTitle("RSI Strategy with Subchart")
        .withSeries(series)
        .withTradingRecordOverlay(record)
        .withSubChart(rsi)
        .toChart();

Performance subchart example

ClosePriceIndicator close = new ClosePriceIndicator(series);
SMAIndicator sma20 = new SMAIndicator(close, 20);
EMAIndicator ema12 = new EMAIndicator(close, 12);

Rule entry = new CrossedUpIndicatorRule(ema12, sma20);
Rule exit  = new CrossedDownIndicatorRule(ema12, sma20);
Strategy strategy = new BaseStrategy("EMA/SMA Crossover", entry, exit);
TradingRecord record = new BarSeriesManager(series).run(strategy);

ChartWorkflow chartWorkflow = new ChartWorkflow();
JFreeChart chart = chartWorkflow.builder()
        .withTitle("Strategy Performance Analysis")
        .withSeries(series)
        .withIndicatorOverlay(sma20)
        .withIndicatorOverlay(ema12)
        .withTradingRecordOverlay(record)
        .withSubChart(new MaximumDrawdownCriterion(), record)
        .toChart();

Multi-indicator chart with multiple subcharts

ClosePriceIndicator close = new ClosePriceIndicator(series);
SMAIndicator sma50  = new SMAIndicator(close, 50);
EMAIndicator ema12  = new EMAIndicator(close, 12);
MACDIndicator macd  = new MACDIndicator(close, 12, 26);
RSIIndicator rsi    = new RSIIndicator(close, 14);

Rule entry = new CrossedUpIndicatorRule(ema12, sma50)
        .and(new OverIndicatorRule(rsi, 50));
Rule exit  = new CrossedDownIndicatorRule(ema12, sma50);
Strategy strategy = new BaseStrategy("Advanced Multi-Indicator Strategy", entry, exit);
TradingRecord record = new BarSeriesManager(series).run(strategy);

ChartWorkflow chartWorkflow = new ChartWorkflow();
JFreeChart chart = chartWorkflow.builder()
        .withTitle("Advanced Multi-Indicator Strategy")
        .withSeries(series)
        .withIndicatorOverlay(sma50)
        .withIndicatorOverlay(ema12)
        .withTradingRecordOverlay(record)
        .withSubChart(macd)                               // MACD subchart
        .withSubChart(rsi)                                // RSI subchart
        .withSubChart(new NetProfitLossCriterion(), record) // profit subchart
        .toChart();

TimeAxisMode

TimeAxisMode.BAR_INDEX compresses non-trading periods (weekends, holidays) by mapping each bar to a sequential integer on the x-axis. The underlying bar timestamps remain unchanged. Use it when you want to avoid the visual whitespace that results from gaps in daily stock data:
chartWorkflow.builder()
        .withTimeAxisMode(TimeAxisMode.BAR_INDEX)
        // ...
Without this option, the default time axis renders actual timestamps, so weekends and holidays produce visible gaps in the chart.

Saving charts

// Saves to output/charts/ema-crossover-strategy.png
chartWorkflow.saveChartImage(chart, series, "ema-crossover-strategy", "output/charts");
The series is used to size the chart width proportionally to the number of bars.

Inspecting a chart plan before rendering

Call toPlan() instead of toChart() to obtain the underlying chart plan. This lets you inspect or modify the configuration — including the shared title, domain series, and time axis mode — before rendering.
ChartPlan plan = chartWorkflow.builder()
        .withTitle("My Strategy")
        .withSeries(series)
        .withTradingRecordOverlay(record)
        .toPlan();

// Inspect what will be rendered
plan.context();   // shared title, time axis mode
plan.metadata();  // chart structure details

// Render when ready
JFreeChart chart = plan.toChart();

Performance metrics

Learn which criteria can be plotted as subcharts.

Parallel backtesting

Run strategy sweeps before charting the best performers.