QFChart v0.6: Drawing Objects, New Plot Styles & Pine Script Rendering Parity

, ,
QFChart v0.6: Drawing Objects, New Plot Styles & Pine Script Rendering Parity

QFChart v0.6 spans eight releases – v0.6.0 through v0.6.8 – that close the gap between QFChart and TradingView’s Pine Script rendering. This cycle adds the four remaining plot styles, fill() support, and a complete drawing object system (labels, lines, linefills). If you’ve been using QFChart since v0.5.0, this post covers every API addition, the architecture changes that enabled them, and the edge-case fixes that came along the way.


What’s Covered in v0.6


New Plot Styles (v0.6.0)

QFChart’s plotting system started with line, histogram, background, shape, and a few others. v0.6.0 added four new styles that complete Pine Script parity for the main visualization functions.

Heikin Ashi and Custom Candlesticks (style: 'candle')

The candle style lets you render a secondary candlestick series as an overlay or in a separate pane – the primary use case being Heikin Ashi candles on top of regular price data.

Each data point’s value is [open, high, low, close]:

// Standalone: Heikin Ashi candles as an overlay
const heikinAshiData = [
    { time: 1700000000000, value: [50000, 51000, 49500, 50500] },
    { time: 1700086400000, value: [50500, 52000, 50000, 51800] },
    { time: 1700172800000, value: [51800, 53000, 51200, 52600] },
    { time: 1700259200000, value: [52600, 53500, 51800, 52000] },
    { time: 1700345600000, value: [52000, 52800, 50800, 51200] },
];

chart.addIndicator('HA', {
    ha: {
        data: heikinAshiData,
        options: {
            style: 'candle',
            color: '#26a69a',   <em>// Bullish body fill</em>
            wickcolor: '#1a7a6e',
            bordercolor: '#145f57',
        },
    },
}, { overlay: true });

Per-point color overrides allow each candle to be colored individually – this is how Pine Script’s plotcandle() conditional coloring works:

// Standalone: conditional candle coloring
const haTrend = [
    {
        time: 1700000000000,
        value: [50000, 51000, 49500, 50500],
        options: { color: '#26a69a', wickcolor: '#1a7a6e' }, <em>// Bullish</em>
    },
    {
        time: 1700086400000,
        value: [50500, 52000, 50000, 51800],
        options: { color: '#26a69a', wickcolor: '#1a7a6e' }, <em>// Bullish</em>
    },
    {
        time: 1700172800000,
        value: [51800, 53000, 51200, 52600],
        options: { color: '#ef5350', wickcolor: '#b71c1c' }, <em>// Bearish reversal</em>
    },
];

chart.addIndicator('HA_Trend', {
    candles: { data: haTrend, options: { style: 'candle', color: '#888' } },
}, { overlay: true });

With PineTS – running a Heikin Ashi Pine Script indicator and feeding the output directly to QFChart:

//@version=6
indicator("Heikin Ashi", overlay=true)

haClose = (open + high + low + close) / 4
haOpen = float(na)
haOpen := na(haOpen[1]) ? (open + close) / 2 : (haOpen[1] + haClose[1]) / 2
haHigh = math.max(high, math.max(haOpen, haClose))
haLow  = math.min(low,  math.min(haOpen, haClose))

isBull = haClose > haOpen
clr = isBull ? color.teal : color.red

plotcandle(haOpen, haHigh, haLow, haClose, "HA", color=clr, wickcolor=clr)
// Feed PineTS output to QFChart
const pineTS = new PineTS(Provider.Binance, 'BTCUSDT', '60', null, startTime, endTime);
const { plots } = await pineTS.run(pineScript);

// PineTS emits plotcandle as style:'candle' with per-point color overrides
chart.addIndicator('HA', plots, { overlay: true });

OHLC Bar Charts (style: 'bar')

The bar style renders traditional OHLC bars – a vertical line from low to high with a left tick for open and a right tick for close. Same [open, high, low, close] data format as candle:

// Standalone: OHLC bars in a separate pane
const ohlcData = [
    { time: 1700000000000, value: [50000, 51000, 49500, 50500] },
    { time: 1700086400000, value: [50500, 52000, 50000, 51800] },
    { time: 1700172800000, value: [51800, 53000, 51200, 52600] },
];

chart.addIndicator('OHLC_Bars', {
    bars: {
        data: ohlcData,
        options: {
            style: 'bar',
            color: '#888888',
            wickcolor: '#555555',
        },
    },
}, { overlay: false, height: 20 });

Dynamic Candle Coloring (style: 'barcolor')

barcolor is unique – it doesn’t create any visual series at all. Instead it recolors the main chart’s candlesticks. This is how Pine Script’s barcolor() function works: a separate indicator output that modifies the appearance of existing candles.

// Standalone: color candles based on a condition
const marketData = [
    { time: 1700000000000, open: 50000, high: 51000, low: 49500, close: 50500, volume: 100 },
    { time: 1700086400000, open: 50500, high: 52000, low: 50000, close: 51800, volume: 140 },
    { time: 1700172800000, open: 51800, high: 52200, low: 50900, close: 51200, volume: 80 },
    { time: 1700259200000, open: 51200, high: 51800, low: 50000, close: 50400, volume: 120 },
    { time: 1700345600000, open: 50400, high: 51200, low: 49800, close: 51000, volume: 110 },
];

chart.setMarketData(marketData);

// Color candles based on whether volume is above average.
// The `value` field is not displayed — barcolor only uses per-point `options.color`
// to recolor the main candle. When `options.color` is absent, the candle keeps its
// default color. Any numeric value (e.g. 0) satisfies the data point requirement.
const volumeColors = {
    trend: {
        data: [
            { time: 1700000000000, value: 0 },                                      // No override — default candle color
            { time: 1700086400000, value: 0, options: { color: '#ff9800' } },       // High volume → orange
            { time: 1700172800000, value: 0 },                                      // No override
            { time: 1700259200000, value: 0, options: { color: '#ff9800' } },       // High volume → orange
            { time: 1700345600000, value: 0 },                                      // No override
        ],
        options: { style: 'barcolor', color: '#888888' },
    },
};

chart.addIndicator('Volume_Color', volumeColors, { overlay: false });

With PineTS – the barcolor() Pine Script call maps directly to style: 'barcolor':

//@version=6
indicator("RSI Bar Color", overlay=true)

rsi = ta.rsi(close, 14)
barcolor(rsi > 70 ? color.red : rsi < 30 ? color.green : na, title="RSI Color")
const { plots } = await pineTS.run(pineScript);
// plots['RSI Color'] will have style: 'barcolor'
chart.addIndicator('RSI_Color', plots, { overlay: true });

Tooltip-Only Data (style: 'char')

The char style – named after Pine Script’s plotchar() – exposes values in the chart tooltip without rendering any visual element. In Pine Script, plotchar() can display characters on the chart, but its most common use is publishing auxiliary data to the Data Window. QFChart’s char style mirrors that tooltip-only behavior. This is useful for debug values, ratios, or auxiliary data you want to inspect on hover without cluttering the chart.

// Standalone: display volume ratio in tooltip only
const volumeRatioData = [
    { time: 1700000000000, value: 1.25 }, // 25% above average
    { time: 1700086400000, value: 1.62 }, // 62% above average
    { time: 1700172800000, value: 0.73 }, // Below average
    { time: 1700259200000, value: 0.91 }, // Below average
    { time: 1700345600000, value: 1.08 }, // Slightly above average
];

chart.addIndicator('Volume_Stats', {
    volRatio: {
        data: volumeRatioData,
        options: { style: 'char', color: '#888888' },
    },
}, { overlay: true });

Plot Fill Between Lines (v0.6.4)

fill() shades the area between two existing plots. This is a must-have for Bollinger Bands, Keltner Channels, Ichimoku clouds, and any indicator with upper/lower bounds.

Unlike other plot styles, a fill plot has no data array – it references two other plots by their key names within the same indicator:

// Standalone: Bollinger Bands with fill
const now = 1700000000000;
const interval = 86400000; // 1 day

const bbPlots = {
    upper: {
        data: [
            { time: now + 0 * interval, value: 52000 },
            { time: now + 1 * interval, value: 52400 },
            { time: now + 2 * interval, value: 52800 },
            { time: now + 3 * interval, value: 52500 },
        ],
        options: { style: 'line', color: '#2196F3', linewidth: 1 },
    },
    basis: {
        data: [
            { time: now + 0 * interval, value: 50500 },
            { time: now + 1 * interval, value: 50700 },
            { time: now + 2 * interval, value: 51000 },
            { time: now + 3 * interval, value: 50800 },
        ],
        options: { style: 'line', color: '#FFC107', linewidth: 2 },
    },
    lower: {
        data: [
            { time: now + 0 * interval, value: 49000 },
            { time: now + 1 * interval, value: 49000 },
            { time: now + 2 * interval, value: 49200 },
            { time: now + 3 * interval, value: 49100 },
        ],
        options: { style: 'line', color: '#2196F3', linewidth: 1 },
    },
    // Fill between upper and lower bands — references plot keys by name
    bbFill: {
        plot1: 'upper',
        plot2: 'lower',
        options: { style: 'fill', color: 'rgba(33, 150, 243, 0.15)' },
    },
};

chart.addIndicator('BB_20', bbPlots, { overlay: true });

With PineTS – fill() in Pine Script maps directly to style: 'fill' in QFChart. Both plot1 and plot2 reference the plot keys that correspond to the titles passed to plot():

//@version=6
indicator("Bollinger Bands", overlay=true)
length = input.int(20, "Length")
mult  = input.float(2.0, "Multiplier")

[middle, upper, lower] = ta.bb(close, length, mult)

plot(middle, "Basis", color=color.yellow, linewidth=2)
p1 = plot(upper, "Upper", color=color.blue, linewidth=1)
p2 = plot(lower, "Lower", color=color.blue, linewidth=1)
fill(p1, p2, title="BB Fill", color=color.new(color.blue, 85))
const { plots } = await pineTS.run(pineScript);
// PineTS emits: plots['Basis'], plots['Upper'], plots['Lower'], plots['BB Fill']
// plots['BB Fill'] = { plot1: 'Upper', plot2: 'Lower', options: { style: 'fill', color: ... } }
chart.addIndicator('BB_20', plots, { overlay: true });

Fills render at z=1 – behind plot lines (z=2) and candles (z=5), but above the grid background (z=0). This means fills are visible on both the main chart pane and in separate panes.

Keltner Channels – Two Fills in One Indicator

Fill can be used multiple times within the same indicator. This is how Keltner Channels are commonly displayed with a green zone above the midline and a red zone below:

// Standalone: Keltner Channels with dual fills
const kcPlots = {
    upper:  { data: upperData,  options: { style: 'line', color: '#4CAF50', linewidth: 1 } },
    middle: { data: middleData, options: { style: 'line', color: '#FFC107', linewidth: 2 } },
    lower:  { data: lowerData,  options: { style: 'line', color: '#F44336', linewidth: 1 } },
    fillUp: {
        plot1: 'middle',
        plot2: 'upper',
        options: { style: 'fill', color: 'rgba(76, 175, 80, 0.12)' },
    },
    fillDown: {
        plot1: 'middle',
        plot2: 'lower',
        options: { style: 'fill', color: 'rgba(244, 67, 54, 0.12)' },
    },
};

chart.addIndicator('KC_20', kcPlots, { overlay: true });

Labels (v0.6.7)

v0.6.7 adds full label rendering support, corresponding to Pine Script’s label.* namespace. Labels display text annotations anchored to specific bars and price levels – useful for marking entry/exit points, key levels, or any event you want to annotate directly on the chart.

Unlike regular plot styles, drawing objects (labels, lines, linefills) use reserved plot keys__labels____lines__, and __linefills__. All objects of the same type are stored in a single data entry whose time must be set to the first bar’s timestamp – this is how QFChart identifies the drawing object entry and associates it with the chart’s time range:

// Standalone: BUY/SELL labels at specific price levels
const marketData = [
    { time: 1700000000000, open: 50000, high: 51000, low: 49000, close: 50500, volume: 100 },
    { time: 1700086400000, open: 50500, high: 52000, low: 50000, close: 51800, volume: 140 },
    { time: 1700172800000, open: 51800, high: 53000, low: 51000, close: 52500, volume: 150 },
    { time: 1700259200000, open: 52500, high: 53500, low: 51500, close: 52000, volume: 130 },
    { time: 1700345600000, open: 52000, high: 52500, low: 50500, close: 51000, volume: 110 },
];

chart.setMarketData(marketData);

chart.addIndicator('Signals', {
    __labels__: {
        data: [{
            time: marketData[0].time,
            value: [
                {
                    x: 1, y: 51800,
                    text: 'BUY',
                    xloc: 'bar_index',
                    yloc: 'price',
                    color: '#26a69a',
                    style: 'style_label_up',
                    textcolor: '#ffffff',
                    size: 'normal',
                    _deleted: false,
                },
                {
                    x: 4, y: 51000,
                    text: 'SELL',
                    xloc: 'bar_index',
                    yloc: 'price',
                    color: '#ef5350',
                    style: 'style_label_down',
                    textcolor: '#ffffff',
                    size: 'normal',
                    _deleted: false,
                },
            ],
            options: { style: 'label' },
        }],
        options: { style: 'label', overlay: true },
    },
}, { overlay: true });

Labels support 15 styles from style_label_up and style_label_down to style_arrowupstyle_flagstyle_circlestyle_diamond, and more. The yloc property controls positioning:

  • 'price' – positioned at the exact y price value
  • 'abovebar' – positioned above the candle’s high (y is ignored)
  • 'belowbar' – positioned below the candle’s low (y is ignored)
// Standalone: signal arrows above/below bars (y is ignored with yloc)
chart.addIndicator('Arrows', {
    __labels__: {
        data: [{
            time: marketData[0].time,
            value: [
                {
                    x: 1, y: 0,         // y ignored for yloc:'belowbar'
                    text: '',
                    xloc: 'bar_index',
                    yloc: 'belowbar',
                    color: '#26a69a',
                    style: 'style_none',
                    textcolor: '#26a69a',
                    size: 'large',
                    _deleted: false,
                },
                {
                    x: 3, y: 0,         // y ignored for yloc:'abovebar'
                    text: '',
                    xloc: 'bar_index',
                    yloc: 'abovebar',
                    color: '#ef5350',
                    style: 'style_none',
                    textcolor: '#ef5350',
                    size: 'large',
                    _deleted: false,
                },
            ],
            options: { style: 'label' },
        }],
        options: { style: 'label', overlay: true },
    },
}, { overlay: true });

With PineTS – label.new() calls in Pine Script flow through the __labels__ reserved plot key automatically:

//@version=6
indicator("EMA Cross Labels", overlay=true)

fast = ta.ema(close, 9)
slow = ta.ema(close, 21)

crossUp   = ta.crossover(fast, slow)
crossDown = ta.crossunder(fast, slow)

if crossUp
    label.new(bar_index, low, "Buy",
        xloc=xloc.bar_index, yloc=yloc.belowbar,
        color=color.teal, style=label.style_label_up,
        textcolor=color.white)

if crossDown
    label.new(bar_index, high, "Sell",
        xloc=xloc.bar_index, yloc=yloc.abovebar,
        color=color.red, style=label.style_label_down,
        textcolor=color.white)

plot(fast, "Fast EMA", color=color.teal)
plot(slow, "Slow EMA", color=color.red)
const { plots } = await pineTS.run(pineScript);
// plots includes 'Fast EMA', 'Slow EMA' (line style) + '__labels__' (label style)
chart.addIndicator('EMA_Cross', plots, { overlay: true });

Drawing Lines (v0.6.8)

v0.6.8 adds DrawingLineRenderer for Pine Script’s line.* namespace. Lines connect two (x, y) points on the chart and support solid, dashed, dotted, and arrow styles, as well as extending to the chart edges.

The key distinction: drawing lines use style: 'drawing_line' (not 'line'). The plain 'line' style is for time-series plots. This avoids collisions between the two.

// Standalone: trend lines on a chart
const marketData = [
    { time: 1700000000000, open: 50000, high: 51000, low: 49000, close: 50500, volume: 100 },
    { time: 1700086400000, open: 50500, high: 52000, low: 50000, close: 51800, volume: 140 },
    { time: 1700172800000, open: 51800, high: 53000, low: 51000, close: 52500, volume: 150 },
    { time: 1700259200000, open: 52500, high: 53500, low: 51500, close: 52000, volume: 130 },
    { time: 1700345600000, open: 52000, high: 52500, low: 50500, close: 51000, volume: 110 },
];

chart.setMarketData(marketData);

chart.addIndicator('Trend', {
    __lines__: {
        data: [{
            time: marketData[0].time,
            value: [
                // Uptrend line connecting lows — extends to the right
                {
                    x1: 0, y1: 49000,
                    x2: 2, y2: 51000,
                    xloc: 'bar_index',
                    extend: 'right',
                    color: '#26a69a',
                    style: 'style_solid',
                    width: 2,
                    _deleted: false,
                },
                // Horizontal resistance — dashed, no extension
                {
                    x1: 0, y1: 53500,
                    x2: 4, y2: 53500,
                    xloc: 'bar_index',
                    extend: 'none',
                    color: '#ef5350',
                    style: 'style_dashed',
                    width: 1,
                    _deleted: false,
                },
            ],
            options: { style: 'drawing_line' },
        }],
        options: { style: 'drawing_line', overlay: true },
    },
}, { overlay: true });

Support & Resistance with Extended Lines

The extend: 'both' mode draws a level that spans the entire visible chart – ideal for key price levels:

// Standalone: support and resistance levels extending across the full chart
chart.addIndicator('Levels', {
    __lines__: {
        data: [{
            time: marketData[0].time,
            value: [
                {
                    x1: 0, y1: 49000, x2: 1, y2: 49000,
                    xloc: 'bar_index', extend: 'both',
                    color: '#26a69a', style: 'style_dotted', width: 1,
                    _deleted: false,
                },
                {
                    x1: 0, y1: 53500, x2: 1, y2: 53500,
                    xloc: 'bar_index', extend: 'both',
                    color: '#ef5350', style: 'style_dotted', width: 1,
                    _deleted: false,
                },
            ],
            options: { style: 'drawing_line' },
        }],
        options: { style: 'drawing_line', overlay: true },
    },
}, { overlay: true });

With PineTS – line.new() calls map directly to __lines__. The PineTS runtime stores all active lines as a single aggregated array (see implementation note below):

//@version=6
indicator("Support & Resistance", overlay=true)

// Draw a horizontal line at each pivot high/low
ph = ta.pivothigh(high, 5, 5)
pl = ta.pivotlow(low,  5, 5)

if not na(ph)
    line.new(bar_index[5], ph, bar_index, ph,
        xloc=xloc.bar_index, extend=extend.right,
        color=color.red, style=line.style_dashed, width=1)

if not na(pl)
    line.new(bar_index[5], pl, bar_index, pl,
        xloc=xloc.bar_index, extend=extend.right,
        color=color.green, style=line.style_dashed, width=1)
const { plots } = await pineTS.run(pineScript);
// plots['__lines__'] contains all line objects as a single aggregated entry
chart.addIndicator('SR_Levels', plots, { overlay: true });

Linefills (v0.6.8)

LinefillRenderer fills the polygon between two line objects. This is the chart equivalent of Pine Script’s linefill.new() – useful for highlighting price channels, ranges, and zones bounded by trend lines.

Linefills use __linefills__ as the plot key and reference full line objects (with all their coordinates) directly:

// Standalone: ascending price channel with fill</em>
const upperLine = {
    x1: 0, y1: 51000, x2: 4, y2: 54000,
    xloc: 'bar_index', extend: 'none',
    color: '#2196F3', style: 'style_solid', width: 2,
    _deleted: false,
};

const lowerLine = {
    x1: 0, y1: 49000, x2: 4, y2: 51500,
    xloc: 'bar_index', extend: 'none',
    color: '#2196F3', style: 'style_solid', width: 2,
    _deleted: false,
};

chart.addIndicator('Channel', {
    // Visible boundary lines</em>
    __lines__: {
        data: [{
            time: marketData[0].time,
            value: [upperLine, lowerLine],
            options: { style: 'drawing_line' },
        }],
        options: { style: 'drawing_line', overlay: true },
    },
    // Polygon fill between the two lines</em>
    __linefills__: {
        data: [{
            time: marketData[0].time,
            value: [{
                line1: upperLine,
                line2: lowerLine,
                color: 'rgba(33, 150, 243, 0.15)',
                _deleted: false,
            }],
            options: { style: 'linefill' },
        }],
        options: { style: 'linefill', overlay: true },
    },
}, { overlay: true });

The linefill renderer reads coordinates directly from the line objects – so if the lines use extend: 'right', the fill polygon extends accordingly. You can share the same line object reference between __lines__ and __linefills__ without duplicating data.

With PineTS – linefill.new(line1, line2, color) is fully supported:

//@version=6</em>
indicator("Price Channel", overlay=true)

// Simple channel: highest high and lowest low over N bars</em>
length = input.int(20, "Channel Length")

var l_upper = line.new(na, na, na, na, extend=extend.right,
    color=color.blue, style=line.style_solid, width=2)
var l_lower = line.new(na, na, na, na, extend=extend.right,
    color=color.blue, style=line.style_solid, width=2)
var lf = linefill.new(l_upper, l_lower, color=color.new(color.blue, 85))

if barstate.islast
    line.set_x1(l_upper, bar_index - length)
    line.set_y1(l_upper, ta.highest(high, length)[1])
    line.set_x2(l_upper, bar_index)
    line.set_y2(l_upper, ta.highest(high, length))

    line.set_x1(l_lower, bar_index - length)
    line.set_y1(l_lower, ta.lowest(low, length)[1])
    line.set_x2(l_lower, bar_index)
    line.set_y2(l_lower, ta.lowest(low, length))
const { plots } = await pineTS.run(pineScript);
// plots['__lines__'] and plots['__linefills__'] are both populated</em>
chart.addIndicator('Channel', plots, { overlay: true });

Mixing Drawing Objects with PineTS

Labels, lines, and linefills can coexist in the same indicator – and when using PineTS, they do so automatically. A Pine Script that uses label.new()line.new(), and linefill.new() together produces a single plots object containing __labels____lines__, and __linefills__ alongside any regular plot series. One call to addIndicator() renders everything:

const { plots } = await pineTS.run(channelScript);
// plots = {</em>
//   'Fast EMA':    { data: [...], options: { style: 'line', ... } },</em>
//   'Slow EMA':    { data: [...], options: { style: 'line', ... } },</em>
//   '__labels__':  { data: [...], options: { style: 'label', ... } },</em>
//   '__lines__':   { data: [...], options: { style: 'drawing_line', ... } },</em>
//   '__linefills__': { data: [...], options: { style: 'linefill', ... } },</em>
// }</em>
chart.addIndicator('Channel_Analysis', plots, { overlay: true });

No manual assembly required – the reserved keys are populated by the PineTS runtime and consumed by QFChart’s renderers as-is.


Z-Level Ordering

With so many overlay types, knowing what renders on top of what matters. Here’s the complete reference:

Z-LevelElement
0Grid background
1fill() between plots (style: 'fill')
2Plot lines (style: 'line''step', etc.)
5Main candlestick series
10Linefill polygons (style: 'linefill')
15Drawing lines (style: 'drawing_line')
20Labels (style: 'label')

Labels always render on top, ensuring text annotations are never obscured. Drawing lines sit above candles, and linefill polygons sit between – providing a natural visual layering without configuration.

One important fix in v0.6.8: the FillRenderer previously used z: -5, which rendered fills behind the grid background – completely invisible on the main price pane. This was fixed to z: 1, making fills visible across all panes.


Architecture Improvements (v0.6.5)

v0.6.5 was a pure internal refactor with no API changes. The key outcome: each plot style now has its own renderer module behind a central SeriesRendererFactory. This means adding a new style – whether it’s a custom visualization or a future Pine Script type – is a single file plus a factory registration, with no risk of breaking existing styles. The drawing object renderers added in v0.6.7–v0.6.8 (labels, lines, linefills) were the first beneficiaries of this architecture.


Reliability Fixes (v0.6.6)

v0.6.6 fixed two real-world edge cases that affected usability:

Small-Price Assets (e.g., meme tokens)

The last-price line was invisible for assets like PUMP/USDT (price ~0.002) because the Y-axis was using 2 decimal places, so the markLine value rounded to 0.00 and didn’t align with any candle. The fix introduced AxisUtils.autoDetectDecimals() which inspects the price magnitude:

// Auto-detection logic (conceptual):</em>
// Price 0.002  → 4-6 decimal places</em>
// Price 45,000 → 0-2 decimal places</em>
// Price 1.25   → 2 decimal places</em>

You can override this with the yAxisDecimalPlaces option:

chart = new QFChart(container, marketData, {
    yAxisDecimalPlaces: 8,  // Manual override for very small prices</em>
});

Overlay Y-axis Contamination

Indicators containing barcolor or background plots were causing the main price Y-axis to start at negative values. The fix ensures visual-only plots (barcolorbackground) always get their own hidden Y-axis with a fixed [0, 1] range, never contaminating the main price axis.


Installation

npm install qfchart@latest

QFChart requires Apache ECharts as a peer dependency:

npm install echarts

Or via CDN (the .min.browser.js bundle is the production build – minified, no sourcemaps):

<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/qfchart/dist/qfchart.min.browser.js"></script>

For development, use the unminified bundle with sourcemaps: qfchart.dev.browser.js.


What’s Next

The v0.6 cycle established drawing object infrastructure. Next focus areas:

  • box.* support – Rectangular drawing objects (Pine Script boxes)
  • table.* support – On-chart data tables
  • polyline.* support – Multi-point drawing objects
  • Strategy output rendering – Visual backtesting results (trades, equity curve)

Resources

Leave a Reply

Your email address will not be published. Required fields are marked *