Skip to main content

Drawing Objects

QFChart supports three types of drawing objects that overlay the chart: labels, lines, and linefills. Unlike plot styles (which display time-series data), drawing objects are positioned at specific coordinates and are ideal for annotations, trend lines, support/resistance levels, and filled regions.

Overview

Drawing objects use reserved plot keys in the indicator's plots object:

Object TypePlot KeyStyleDescription
Labels__labels__'label'Text annotations with bubble/shape markers
Lines__lines__'drawing_line'Line segments between two points
Linefills__linefills__'linefill'Filled polygons between two lines

All drawing objects are rendered as overlays on the main chart pane by default.


Data Format

Drawing objects differ from regular plots in a key way: instead of one data point per timestamp, all objects are stored as a single array in one data entry. This is because multiple drawing objects can share the same timestamp (e.g., several lines created at bar 0), and storing them separately would cause only the last one to survive in the sparse data array.

// General pattern for all drawing objects
'__plotKey__': {
data: [{
time: marketData[0].time, // Timestamp of the first bar
value: [ /* array of objects */ ],
options: { style: '...' }
}],
options: { style: '...', overlay: true }
}

Labels (style: 'label')

Labels display text annotations with customizable shapes, colors, and positioning. They can be placed at exact price levels or relative to candle highs/lows.

Label Object Properties

PropertyTypeDefaultDescription
xnumberBar index (0-based position in market data)
ynumberPrice value (Y-coordinate)
textstring''Text to display
xlocstring'bar_index'X-axis mode: 'bar_index'
ylocstring'price'Y-axis mode: 'price', 'abovebar', or 'belowbar'
colorstring'#2962ff'Background/shape color
stylestring'style_label_down'Label style (see table below)
textcolorstring'#ffffff'Text color
sizestring'normal'Size: 'tiny', 'small', 'normal', 'large', 'huge'
textalignstring'align_center'Text alignment: 'align_left', 'align_center', 'align_right'
tooltipstring''Tooltip text on hover
_deletedbooleanfalseSet to true to hide the label

Label Styles

StyleDescription
style_label_downBubble with downward pointer (default)
style_label_upBubble with upward pointer
style_label_leftBubble with left pointer
style_label_rightBubble with right pointer
style_circleCircle marker
style_squareSquare marker
style_diamondDiamond marker
style_flagFlag marker
style_arrowupUpward arrow
style_arrowdownDownward arrow
style_crossCross (+) marker
style_xcrossX-cross marker
style_triangleupUpward triangle
style_triangledownDownward triangle
style_noneNo shape (text only)
style_text_outlineText outline (no shape)

Y-Location Modes

ModeBehavior
pricePositioned at the exact y value
abovebarPositioned above the candle's high (ignores y)
belowbarPositioned below the candle's low (ignores y)

Example: Basic Labels

const marketData = [
{ time: 1620000000000, open: 50000, high: 51000, low: 49000, close: 50500, volume: 100 },
{ time: 1620086400000, open: 50500, high: 52000, low: 50000, close: 51500, volume: 120 },
{ time: 1620172800000, open: 51500, high: 53000, low: 51000, close: 52500, volume: 150 },
{ time: 1620259200000, open: 52500, high: 53500, low: 51500, close: 52000, volume: 130 },
{ time: 1620345600000, open: 52000, high: 52500, low: 50500, close: 51000, volume: 110 },
];

chart.setMarketData(marketData);

const plots = {
__labels__: {
data: [{
time: marketData[0].time,
value: [
{
x: 1, y: 52000,
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 },
},
};

chart.addIndicator('signals', plots, { overlay: true });

Example: Labels Above/Below Bars

When using yloc: 'abovebar' or yloc: 'belowbar', the label is positioned relative to the candle's high or low. The y value is ignored.

const plots = {
__labels__: {
data: [{
time: marketData[0].time,
value: [
{
x: 1,
y: 0, // Ignored when yloc is 'abovebar'
text: '▲',
xloc: 'bar_index',
yloc: 'belowbar',
color: '#26a69a',
style: 'style_none',
textcolor: '#26a69a',
size: 'large',
_deleted: false,
},
{
x: 4,
y: 0, // Ignored when yloc is 'belowbar'
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 },
},
};

chart.addIndicator('arrows', plots, { overlay: true });

Example: Price Annotations with Tooltips

const plots = {
__labels__: {
data: [{
time: marketData[0].time,
value: [
{
x: 2, y: 53000,
text: 'ATH',
xloc: 'bar_index',
yloc: 'price',
color: '#ff9800',
style: 'style_label_down',
textcolor: '#ffffff',
size: 'large',
textalign: 'align_center',
tooltip: 'All-Time High reached on this bar',
_deleted: false,
},
],
options: { style: 'label' },
}],
options: { style: 'label', overlay: true },
},
};

chart.addIndicator('annotations', plots, { overlay: true });

Lines (style: 'drawing_line')

Lines draw segments between two points on the chart. They support dashed/dotted styles, arrow heads, and can be extended to the chart edges.

The style name is 'drawing_line' (not 'line'). The 'line' style is used by regular plot series. This distinction prevents conflicts between indicator lines and drawing lines.

Line Object Properties

PropertyTypeDefaultDescription
x1numberStart bar index (0-based)
y1numberStart price value
x2numberEnd bar index (0-based)
y2numberEnd price value
xlocstring'bar_index'X-axis mode: 'bar_index'
extendstring'none'Line extension: 'none', 'left', 'right', 'both'
colorstring'#2962ff'Line color
stylestring'style_solid'Line style (see table below)
widthnumber1Line width in pixels
_deletedbooleanfalseSet to true to hide the line

Line Styles

StyleDescription
style_solidSolid line (default)
style_dottedDotted line
style_dashedDashed line
style_arrow_leftSolid line with arrow at start point
style_arrow_rightSolid line with arrow at end point
style_arrow_bothSolid line with arrows at both ends

Extend Modes

ModeDescription
noneLine only between (x1,y1) and (x2,y2)
leftExtends infinitely to the left from (x1,y1)
rightExtends infinitely to the right from (x2,y2)
bothExtends infinitely in both directions

Example: Trend Lines

const marketData = [
{ time: 1620000000000, open: 50000, high: 51000, low: 49000, close: 50500, volume: 100 },
{ time: 1620086400000, open: 50500, high: 52000, low: 50000, close: 51500, volume: 120 },
{ time: 1620172800000, open: 51500, high: 53000, low: 51000, close: 52500, volume: 150 },
{ time: 1620259200000, open: 52500, high: 53500, low: 51500, close: 52000, volume: 130 },
{ time: 1620345600000, open: 52000, high: 52500, low: 50500, close: 51000, volume: 110 },
];

chart.setMarketData(marketData);

const plots = {
__lines__: {
data: [{
time: marketData[0].time,
value: [
// Uptrend line connecting lows
{
x1: 0, y1: 49000,
x2: 2, y2: 51000,
xloc: 'bar_index',
extend: 'right',
color: '#26a69a',
style: 'style_solid',
width: 2,
_deleted: false,
},
// Horizontal resistance level
{
x1: 0, y1: 53000,
x2: 4, y2: 53000,
xloc: 'bar_index',
extend: 'none',
color: '#ef5350',
style: 'style_dashed',
width: 1,
_deleted: false,
},
],
options: { style: 'drawing_line' },
}],
options: { style: 'drawing_line', overlay: true },
},
};

chart.addIndicator('trendlines', plots, { overlay: true });

Example: Arrow Lines

const plots = {
__lines__: {
data: [{
time: marketData[0].time,
value: [
// Arrow pointing to a price move
{
x1: 1, y1: 50000,
x2: 3, y2: 53500,
xloc: 'bar_index',
extend: 'none',
color: '#26a69a',
style: 'style_arrow_right',
width: 2,
_deleted: false,
},
// Bidirectional arrow showing range
{
x1: 0, y1: 48000,
x2: 4, y2: 48000,
xloc: 'bar_index',
extend: 'none',
color: '#7e57c2',
style: 'style_arrow_both',
width: 1,
_deleted: false,
},
],
options: { style: 'drawing_line' },
}],
options: { style: 'drawing_line', overlay: true },
},
};

chart.addIndicator('arrows', plots, { overlay: true });

Example: Support & Resistance with Extended Lines

const plots = {
__lines__: {
data: [{
time: marketData[0].time,
value: [
// Support level (extends both directions)
{
x1: 0, y1: 49000,
x2: 1, y2: 49000,
xloc: 'bar_index',
extend: 'both',
color: '#26a69a',
style: 'style_dotted',
width: 1,
_deleted: false,
},
// Resistance level (extends both directions)
{
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 },
},
};

chart.addIndicator('levels', plots, { overlay: true });

Linefills (style: 'linefill')

Linefills fill the area between two line objects with a colored polygon. They are useful for highlighting price channels, ranges, or zones between trend lines.

Linefill Object Properties

PropertyTypeDefaultDescription
line1objectFirst line object (same structure as a line)
line2objectSecond line object (same structure as a line)
colorstring'rgba(128, 128, 128, 0.2)'Fill color (supports rgba for transparency)
_deletedbooleanfalseSet to true to hide the linefill

The line1 and line2 properties must be full line objects (with x1, y1, x2, y2, xloc, extend, etc.). The linefill renderer reads the line coordinates directly from these objects.

The lines referenced by a linefill do not need to be separately added as __lines__ entries. The linefill renderer extracts coordinates directly from the embedded line objects. However, if you want the lines themselves to be visible, you should also add them to __lines__.

Example: Price Channel Fill

const marketData = [
{ time: 1620000000000, open: 50000, high: 51000, low: 49000, close: 50500, volume: 100 },
{ time: 1620086400000, open: 50500, high: 52000, low: 50000, close: 51500, volume: 120 },
{ time: 1620172800000, open: 51500, high: 53000, low: 51000, close: 52500, volume: 150 },
{ time: 1620259200000, open: 52500, high: 53500, low: 51500, close: 52000, volume: 130 },
{ time: 1620345600000, open: 52000, high: 52500, low: 50500, close: 51000, volume: 110 },
];

chart.setMarketData(marketData);

// Define the two boundary lines
const upperLine = {
x1: 0, y1: 51000,
x2: 4, y2: 53500,
xloc: 'bar_index',
extend: 'none',
color: '#2196F3',
style: 'style_solid',
width: 2,
_deleted: false,
};

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

const plots = {
// Visible lines
__lines__: {
data: [{
time: marketData[0].time,
value: [upperLine, lowerLine],
options: { style: 'drawing_line' },
}],
options: { style: 'drawing_line', overlay: true },
},
// Fill between the lines
__linefills__: {
data: [{
time: marketData[0].time,
value: [{
line1: upperLine,
line2: lowerLine,
color: 'rgba(33, 150, 243, 0.2)',
_deleted: false,
}],
options: { style: 'linefill' },
}],
options: { style: 'linefill', overlay: true },
},
};

chart.addIndicator('channel', plots, { overlay: true });

Example: Highlighted Zone with Extended Lines

When lines use extend, the linefill polygon extends accordingly:

const support = {
x1: 0, y1: 49500,
x2: 2, y2: 50500,
xloc: 'bar_index',
extend: 'right',
color: '#26a69a',
style: 'style_dashed',
width: 1,
_deleted: false,
};

const resistance = {
x1: 0, y1: 52000,
x2: 2, y2: 53000,
xloc: 'bar_index',
extend: 'right',
color: '#ef5350',
style: 'style_dashed',
width: 1,
_deleted: false,
};

const plots = {
__lines__: {
data: [{
time: marketData[0].time,
value: [support, resistance],
options: { style: 'drawing_line' },
}],
options: { style: 'drawing_line', overlay: true },
},
__linefills__: {
data: [{
time: marketData[0].time,
value: [{
line1: support,
line2: resistance,
color: 'rgba(255, 235, 59, 0.15)',
_deleted: false,
}],
options: { style: 'linefill' },
}],
options: { style: 'linefill', overlay: true },
},
};

chart.addIndicator('zone', plots, { overlay: true });

Combining All Drawing Objects

You can combine labels, lines, and linefills in a single indicator:

const marketData = [
{ time: 1620000000000, open: 50000, high: 51000, low: 49000, close: 50500, volume: 100 },
{ time: 1620086400000, open: 50500, high: 52000, low: 50000, close: 51500, volume: 120 },
{ time: 1620172800000, open: 51500, high: 53000, low: 51000, close: 52500, volume: 150 },
{ time: 1620259200000, open: 52500, high: 53500, low: 51500, close: 52000, volume: 130 },
{ time: 1620345600000, open: 52000, high: 52500, low: 50500, close: 51000, volume: 110 },
];

chart.setMarketData(marketData);

// Define lines for reuse in linefill
const trendUpper = {
x1: 0, y1: 51000, x2: 4, y2: 53000,
xloc: 'bar_index', extend: 'none',
color: '#26a69a', style: 'style_solid', width: 2,
_deleted: false,
};

const trendLower = {
x1: 0, y1: 49000, x2: 4, y2: 51000,
xloc: 'bar_index', extend: 'none',
color: '#26a69a', style: 'style_solid', width: 2,
_deleted: false,
};

const plots = {
__lines__: {
data: [{
time: marketData[0].time,
value: [trendUpper, trendLower],
options: { style: 'drawing_line' },
}],
options: { style: 'drawing_line', overlay: true },
},
__linefills__: {
data: [{
time: marketData[0].time,
value: [{
line1: trendUpper,
line2: trendLower,
color: 'rgba(38, 166, 154, 0.15)',
_deleted: false,
}],
options: { style: 'linefill' },
}],
options: { style: 'linefill', overlay: true },
},
__labels__: {
data: [{
time: marketData[0].time,
value: [
{
x: 2, y: 53000,
text: 'Resistance',
xloc: 'bar_index', yloc: 'price',
color: '#ef5350', style: 'style_label_down',
textcolor: '#ffffff', size: 'small',
_deleted: false,
},
{
x: 2, y: 49500,
text: 'Support',
xloc: 'bar_index', yloc: 'price',
color: '#26a69a', style: 'style_label_up',
textcolor: '#ffffff', size: 'small',
_deleted: false,
},
],
options: { style: 'label' },
}],
options: { style: 'label', overlay: true },
},
};

chart.addIndicator('analysis', plots, { overlay: true });

Z-Level Ordering

Drawing objects render at specific z-levels to maintain proper visual layering:

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

Labels always render on top, followed by drawing lines, then linefills. This ensures text annotations are never hidden behind lines or fills.


Coordinate System

Bar Index

All drawing objects use xloc: 'bar_index' by default. The x, x1, and x2 values correspond to the 0-based index into the market data array:

  • Bar index 0 = first candle in marketData
  • Bar index marketData.length - 1 = last candle

QFChart internally adds padding candles before and after the real data. The renderers automatically apply the padding offset — you should always use real data indices (0 to N-1) in your drawing objects.

Price Values

The y, y1, and y2 values are absolute price values on the Y-axis. They correspond to the same scale as the candlestick data (open, high, low, close).


Best Practices

  1. Always set _deleted: false on new objects. The renderers skip any object with _deleted: true.

  2. Use shared line object references for linefills. Since linefills read coordinates directly from their line1/line2 objects, modifying the line object after creation automatically updates the fill.

  3. Store all objects in a single data entry at the first bar's timestamp. Do not create separate data entries per object — they will overwrite each other in the sparse data array.

  4. Use overlay: true in the plot options. Drawing objects are typically rendered on the main chart pane alongside candlesticks.

  5. Use rgba colors for linefills to keep the fill semi-transparent and avoid obscuring the candles underneath.

  6. Combine with regular plots — drawing objects can coexist with regular plot styles (line, histogram, etc.) in the same indicator.


See Also