Pipsend

Pipsend Charts for the Web

Documentation for @pipsend/charts, the JavaScript and TypeScript charting library for trading apps, with candlestick and line series, 23 technical indicators, interactive drawing tools, trading lines, and chart serialization.

Ver MarkdownLa documentación técnica está en inglés.
Pipsend Charts for the Web

Overview

@pipsend/charts is a financial charting library for web applications, written in TypeScript and rendered on HTML canvas. It is framework-agnostic: you can use it from vanilla JavaScript, React, Vue, Angular, Svelte, or any other environment that gives you a DOM element.

It is built on top of TradingView's Lightweight Charts and adds a trading-oriented layer:

  • Series types: candlestick, bar (OHLC), line, area, baseline, histogram, and custom series, plus a Heikin Ashi transform.
  • 23 technical indicators applied with one function call each (SMA, EMA, RSI, MACD, Bollinger Bands, Ichimoku, VWAP and more). They recalculate on their own when the source data changes.
  • Interactive drawing tools: position, ruler, Fibonacci, lines, channels, shapes, brush, and text, managed by a DrawingToolsManager with selection, undo/redo and keyboard shortcuts.
  • Trading features: draggable price lines (stop loss, take profit, entry), position lines with P&L labels, trade markers, and click-to-create lines.
  • Serialization: every drawing tool can be saved to plain JSON and restored, which enables chart templates and cross-device sync.
  • Theming: colors, grid, crosshair, fonts and time zone are all configurable at runtime.

Package details (from package.json):

Package @pipsend/charts
Version documented here 2.5.0
Module format ES module (type: "module"), with a standalone IIFE build for <script> tags
Types Bundled (dist/typings.d.ts)
Runtime dependency fancy-canvas

Installation

Package managers

bash
# npm
npm install @pipsend/charts

# yarn
yarn add @pipsend/charts

# pnpm
pnpm add @pipsend/charts

# bun
bun add @pipsend/charts

CDN

The standalone builds bundle their dependencies and expose everything on a global PipsendCharts object (a LightweightCharts alias to the same object is also provided for backward compatibility).

html
<!-- Production (Minified) -->
<script src="https://unpkg.com/@pipsend/charts/dist/pipsend-charts.standalone.production.js"></script>

<!-- Development (with warnings) -->
<script src="https://unpkg.com/@pipsend/charts/dist/pipsend-charts.standalone.development.js"></script>

Build variants

Dependencies bundled Mode ES module IIFE
No Production pipsend-charts.production.mjs N/A
No Development pipsend-charts.development.mjs N/A
Yes Production pipsend-charts.standalone.production.mjs pipsend-charts.standalone.production.js
Yes Development pipsend-charts.standalone.development.mjs pipsend-charts.standalone.development.js

Use the production build in production. The package exports map resolves to it by default.

Requirements

  • A modern browser with ES2015+ and the Canvas API. The README lists these minimum versions: Chrome 63+, Firefox 67+, Safari 11.1+, Edge 79+, Opera 50+, iOS Safari 11.3+, Chrome Android 63+.
  • Node.js 22.3 or newer is declared in engines for building the library from source.
  • Charts need a browser environment (canvas). In server-rendered frameworks, create the chart on the client only (see Framework integration).

Quick start

Basic candlestick chart

javascript
import { createChart, CandlestickSeries } from '@pipsend/charts';

const chart = createChart(document.getElementById('chart'), {
    width: 800,
    height: 600
});

const candleSeries = chart.addSeries(CandlestickSeries, {
    upColor: '#26a69a',
    downColor: '#ef5350'
});

candleSeries.setData([
    { time: '2024-01-01', open: 100, high: 105, low: 95, close: 102 },
    { time: '2024-01-02', open: 102, high: 108, low: 100, close: 106 },
]);

Adding technical indicators

Indicators take the series first and the chart second: apply<Name>(series, chart, options?).

javascript
import {
    createChart,
    CandlestickSeries,
    applySMA,
    applyRSI,
    applyMACD
} from '@pipsend/charts';

const chart = createChart(document.getElementById('chart'));
const series = chart.addSeries(CandlestickSeries);
series.setData(historicalData);

// Add SMA (Simple Moving Average) - appears on main chart
applySMA(series, chart, { period: 20, color: '#2196F3' });

// Add RSI - appears in separate panel
applyRSI(series, chart, { period: 14, color: '#9C27B0' });

// Add MACD - appears in separate panel
applyMACD(series, chart, {
    fastPeriod: 12,
    slowPeriod: 26,
    signalPeriod: 9
});

Interactive trading lines

javascript
import {
    createTradingLine,
    createInteractiveLineManager,
    LineStyle
} from '@pipsend/charts';

// Stop Loss (draggable by default)
const stopLoss = createTradingLine(series, chart, {
    price: 95.00,
    type: 'stop-loss',
    onDragEnd: (price) => {
        console.log('Stop Loss updated:', price);
        // Update your order in backend
    }
});

// Take Profit (draggable by default)
const takeProfit = createTradingLine(series, chart, {
    price: 110.00,
    type: 'take-profit',
    onDragEnd: (price) => console.log('TP:', price)
});

// Price Alert - Fully customizable
const alert = createTradingLine(series, chart, {
    price: 100.00,
    title: 'Price Alert',
    color: '#FFC107',
    lineStyle: LineStyle.Dotted,
    onDragEnd: (price) => api.updateAlert(price)
});

// Click-to-create: Create lines by clicking on chart
const manager = createInteractiveLineManager(chart, series);

document.getElementById('btnSL').onclick = async () => {
    await manager.enableClickToCreate('stop-loss');
};

Using the standalone build from a script tag

html
<div id="chart" style="width: 100%; height: 600px;"></div>
<script src="https://unpkg.com/@pipsend/charts/dist/pipsend-charts.standalone.production.js"></script>
<script>
    const chart = PipsendCharts.createChart(document.getElementById('chart'), {
        height: 600,
        layout: { background: { color: '#ffffff' }, textColor: '#333' },
        timeScale: { timeVisible: true, secondsVisible: false },
    });

    const series = chart.addSeries(PipsendCharts.CandlestickSeries, {
        upColor: '#26a69a',
        downColor: '#ef5350',
    });
    series.setData(candles);
    chart.timeScale().fitContent();
</script>

Creating a chart

typescript
import { createChart } from '@pipsend/charts';

const chart = createChart(container: string | HTMLElement, options?: DeepPartial<ChartOptions>);

container is an element or an element ID. options is any subset of the chart options and can be changed later with chart.applyOptions(...). The container needs explicit dimensions (or set autoSize: true and size the container with CSS).

javascript
const chart = createChart(document.getElementById('chart'), {
    width: 800,
    height: 600,
    layout: {
        background: { color: '#1E222D' },
        textColor: '#DDD',
    },
    grid: {
        vertLines: { color: '#2B2B43' },
        horzLines: { color: '#363C4E' },
    },
});

Main option groups and defaults

Option Default Purpose
width, height 0 Chart size in pixels. 0 means not set.
autoSize false Resize the chart with its container.
addDefaultPane true Create the first pane automatically.
layout.background { type: ColorType.Solid, color: '#FFFFFF' } Chart background.
layout.textColor '#191919' Axis and label text color.
layout.fontSize 12 Font size.
layout.panes.enableResize true Let users drag pane separators.
layout.attributionLogo true Shows the Pipsend logo in the bottom-left corner, linking to pipsend.com.
grid.vertLines, grid.horzLines { color: '#D6DCDE', style: LineStyle.Solid, visible: true } Grid lines.
crosshair.mode CrosshairMode.Magnet Also Normal, Hidden, MagnetOHLC.
rightPriceScale visible Right price axis options.
leftPriceScale hidden Left price axis options.
timeScale see Time scale Horizontal axis options.
localization.locale navigator.language Locale for labels.
localization.dateFormat "dd MMM 'yy" Date format.
localization.timeZone not set IANA time zone, for example 'America/New_York'.
handleScroll all true (mouseWheel, pressedMouseMove, horzTouchDrag, vertTouchDrag) Scroll interactions.
handleScale all true (axisPressedMouseMove, axisDoubleClickReset, mouseWheel, pinch) Zoom interactions.
kineticScroll { mouse: false, touch: true } Inertial scrolling.

Chart API

Method Description
chart.addSeries(definition, options?, paneIndex?) Add a series.
chart.addCustomSeries(paneView, options?) Add a custom series.
chart.removeSeries(series) Remove a series.
chart.applyOptions(options) / chart.options() Change or read chart options.
chart.timeScale() Time scale API.
chart.priceScale(id, paneIndex?) Price scale API.
chart.addPane(), chart.panes(), chart.removePane(i), chart.swapPanes(a, b) Pane management.
chart.resize(width, height, forceRepaint?) Resize manually.
chart.takeScreenshot(addTopLayer?, includeCrosshair?) Returns an HTMLCanvasElement.
chart.subscribeClick(handler), subscribeDblClick, subscribeCrosshairMove Mouse events (each has an unsubscribe* counterpart).
chart.setCrosshairPosition(price, time, series) / chart.clearCrosshairPosition() Move the crosshair programmatically.
chart.chartElement() The root DOM element.
chart.remove() Destroy the chart and release resources.

The library also exports version(), which returns the library version as a string.

Chart types and series

Every series is added with chart.addSeries(<Definition>, options?, paneIndex?).

Series Import Data shape Use
Candlestick CandlestickSeries { time, open, high, low, close } OHLC price. The default for trading.
Bar (OHLC) BarSeries { time, open, high, low, close } OHLC price, denser than candles.
Line LineSeries { time, value } One value per time: closes, indicators.
Area AreaSeries { time, value } Like Line, with a gradient fill.
Baseline BaselineSeries { time, value } A value colored by whether it is above or below a level.
Histogram HistogramSeries { time, value, color? } Vertical bars: volume, MACD.
Custom chart.addCustomSeries(paneView) your own Renderers the library does not ship.

Heikin Ashi is not a series type. It is a transform applied on top of a candlestick or bar series (see Heikin Ashi).

Time values

The time field accepts three forms. Use only one form within a series.

javascript
1529899200                       // UTCTimestamp: UNIX seconds (not milliseconds)
'2021-02-03'                     // ISO business-day string
{ year: 2021, month: 2, day: 3 } // BusinessDay

Loading and updating data

Method When to use it
series.setData(array) Initial load or full replacement of the history.
series.update(point) A new tick: updates the last bar or appends a new one.
series.update(point, true) Correct a bar that is already in the past (historicalUpdate).
series.data() Read what the series currently holds.
series.pop(n) Remove the last n points.

Data rules

These apply to every series type:

  1. Points must be in ascending time order.
  2. Times must be unique. Two points with the same time are an error, not a replacement.
  3. Use one time form per series (timestamps or business days, not mixed).
  4. Timestamps are in seconds, not milliseconds (Date.now() / 1000).
  5. update() only moves forward: the last bar or a new one. Use update(point, true) to touch history.
  6. If two series share an axis, fill the times one of them is missing with whitespace points.

Whitespace (gaps)

A point with only time and no value reserves space on the axis without drawing anything. It is useful for holidays, session gaps, or aligning series of different lengths.

javascript
series.setData([
    { time: '2024-01-02', value: 27.02 },
    { time: '2024-01-03' },              // gap
    { time: '2024-01-04', value: 23.92 }
]);

Options shared by all series

Option Default Purpose
visible true Hides the whole series, including labels and markers.
title '' Name shown on the axis label.
priceScaleId 'right' Which scale the series lives on. '' makes it an overlay.
priceFormat { type: 'price', precision: 2, minMove: 0.01 } How prices are formatted.
lastValueVisible true Last value label on the axis.
priceLineVisible true Dashed horizontal line at the last price.
priceLineColor, priceLineWidth, priceLineStyle '', 1, LineStyle.Dashed Style of that line.
baseLineVisible true Base line.
autoscaleInfoProvider none Custom vertical range instead of the automatic one.

Change them at runtime with series.applyOptions({ ... }) and read them with series.options().

priceFormat.type can be 'price', 'volume', 'percent' or 'custom' (with your own formatter):

javascript
series.applyOptions({
    priceFormat: { type: 'price', precision: 5, minMove: 0.00001 }  // FX
});

Candlestick

javascript
import { CandlestickSeries } from '@pipsend/charts';

const series = chart.addSeries(CandlestickSeries, {
    upColor: '#26a69a',
    downColor: '#ef5350',
    borderVisible: true,
    borderUpColor: '#26a69a',
    borderDownColor: '#ef5350',
    wickVisible: true,
    wickUpColor: '#26a69a',
    wickDownColor: '#ef5350'
});

series.setData([
    { time: '2024-01-02', open: 100, high: 105, low: 99, close: 104 },
    { time: '2024-01-03', open: 104, high: 108, low: 103, close: 106 }
]);

Defaults: upColor '#26a69a', downColor '#ef5350', wickVisible true, borderVisible true, border and wick colors matching up/down colors.

borderColor and wickColor also exist and override the up/down pairs when set, which is useful for a theme where all wicks are the same gray.

Color per candle. Any point can carry its own color, borderColor and wickColor, which win over the series options. Use it to mark specific bars without creating another series:

javascript
{ time: '2024-01-04', open: 106, high: 110, low: 105, close: 109, color: '#ffb300' }

Bar (OHLC)

The same information as candlesticks, drawn as bars with ticks. It takes less width, so it works better with many bars on screen.

javascript
import { BarSeries } from '@pipsend/charts';

const series = chart.addSeries(BarSeries, {
    upColor: '#26a69a',
    downColor: '#ef5350',
    openVisible: true,   // the left tick (open)
    thinBars: true       // 1px bars instead of scaled ones
});

The data is the same as for candlesticks, and each bar accepts its own color.

Line

javascript
import { LineSeries, LineStyle, LineType } from '@pipsend/charts';

const series = chart.addSeries(LineSeries, {
    color: '#2196f3',
    lineWidth: 2,               // only 1 | 2 | 3 | 4
    lineStyle: LineStyle.Solid, // Solid, Dotted, Dashed, LargeDashed, SparseDotted
    lineType: LineType.Simple,  // Simple, WithSteps, Curved
    lineVisible: true,
    pointMarkersVisible: false
});

series.setData([
    { time: '2024-01-02', value: 104 },
    { time: '2024-01-03', value: 106 }
]);

Common requests:

  • Steps (rates, levels that change abruptly): lineType: LineType.WithSteps.
  • Smooth curve: lineType: LineType.Curved.
  • Visible points on sparse series: pointMarkersVisible: true, optionally pointMarkersRadius.
  • Different colors per segment: set color per point. A point's color paints the segment that leaves it toward the next point, so to tint a segment, color its first point.

The marker under the crosshair is controlled by crosshairMarkerVisible, crosshairMarkerRadius, crosshairMarkerBorderColor, crosshairMarkerBackgroundColor and crosshairMarkerBorderWidth.

Area

Line with a gradient fill down to the pane edge. Use it to show magnitude, not to compare several overlapping series (the fills cover each other).

javascript
import { AreaSeries } from '@pipsend/charts';

const series = chart.addSeries(AreaSeries, {
    lineColor: '#33D778',
    topColor: 'rgba(46, 220, 135, 0.4)',
    bottomColor: 'rgba(40, 221, 100, 0)',
    lineWidth: 2
});
Option Purpose
relativeGradient Measures the gradient against the visible range instead of the pane.
invertFilledArea Fills upward instead of downward.

Area inherits the line options. Each point can carry its own lineColor, topColor and bottomColor.

Baseline

A value painted one color above a level and another below it. It suits P&L, performance against an entry price, or anything where the sign matters.

javascript
import { BaselineSeries } from '@pipsend/charts';

const series = chart.addSeries(BaselineSeries, {
    baseValue: { type: 'price', price: 100 },   // the cut-off level
    topLineColor: 'rgba(38, 166, 154, 1)',
    topFillColor1: 'rgba(38, 166, 154, 0.28)',
    topFillColor2: 'rgba(38, 166, 154, 0.05)',
    bottomLineColor: 'rgba(239, 83, 80, 1)',
    bottomFillColor1: 'rgba(239, 83, 80, 0.05)',
    bottomFillColor2: 'rgba(239, 83, 80, 0.28)'
});

baseValue is { type: 'price', price: <number> } and defaults to 0. If you do not set it to a real price, everything is painted in the "top" color. This is the most common mistake with this series. topFillColor1 and topFillColor2 are the two ends of the upper gradient (1 next to the line, 2 away from it); the same applies below.

Histogram

Vertical bars from a base. Use it for volume, MACD, or anything discrete.

javascript
import { HistogramSeries } from '@pipsend/charts';

const series = chart.addSeries(HistogramSeries, {
    color: '#26a69a',
    base: 0,                          // where the bars grow from
    priceFormat: { type: 'volume' }   // formats 1.2M instead of 1200000.00
});

series.setData([
    { time: '2024-01-02', value: 1_200_000, color: '#26a69a' },
    { time: '2024-01-03', value:   900_000, color: '#ef5350' }
]);

Color is per bar, not per series. That is the mechanism for painting volume green or red according to the candle direction. For MACD, base: 0 with negative values draws the bars downward automatically. For volume specifically, see the Volume indicator.

Custom series

For types the library does not ship (Renko, Kagi, Point and Figure), implement an ICustomSeriesPaneView and register it:

javascript
const series = chart.addCustomSeries(myPaneView, { color: '#2196f3' });

The pane view must implement:

Method Responsibility
renderer() Returns the object that draws on the canvas.
update(data, options) Receives new data and options before each draw.
priceValueBuilder(plotRow) Which prices of each point take part in autoscale.
isWhitespace(data) Tells a real point from a gap.
defaultOptions() The series' default options.

Renko and Kagi also change the time axis (one bar is not one period), so plan the time mapping before writing the renderer.

Heikin Ashi

Heikin Ashi is a transform of the OHLC bars. Each candle averages with the previous one, which removes much of the bar-to-bar noise.

text
close = (O + H + L + C) / 4
open  = (previous open + previous close) / 2      // first bar: (O + C) / 2
high  = max(H, open, close)
low   = min(L, open, close)

Apply it on an existing candlestick or bar series:

javascript
import { createChart, CandlestickSeries, applyHeikinAshi } from '@pipsend/charts';

const series = chart.addSeries(CandlestickSeries);
series.setData(bars);

const ha = applyHeikinAshi(series, chart);

// Back to regular candles
ha.remove();

The source series keeps its real prices and is only painted transparent, not hidden. This means:

  • Indicators bound to it keep computing on the true OHLC.
  • Its price lines and primitives (positions, alerts, drawings) stay on screen.
  • remove() restores the chart exactly as it was.
  • New bars pushed to the source are transformed automatically.

This makes it usable as a chart-type toggle in a toolbar. (visible: false would not work for this, because it also turns off everything attached to the series.)

Colors and behavior can be configured:

javascript
applyHeikinAshi(series, chart, {
    upColor: '#26a69a',
    downColor: '#ef5350',
    borderVisible: true,
    wickVisible: true,
    hideSource: true,   // set false to draw over the raw candles
    paneIndex: 0
});

It returns the standard indicator handle (see Indicators): series, applyOptions(), options(), recalculate() and remove().

If you only want the numbers, use the pure function:

javascript
import { calculateHeikinAshi } from '@pipsend/charts';

const haBars = calculateHeikinAshi(bars);

Whitespace points pass through unchanged and do not carry the average. Each Heikin Ashi bar folds in the one before it, so the result depends on where the data starts. Pass the whole series rather than a recent window when you can; the difference decays over a few bars.

Recipes

Switching chart type. There is no setType(), and deleting a series to create another orphans everything attached to it (indicators, price lines, drawings). The approach that works is a source series that is never destroyed, with a view on top, as Heikin Ashi does:

javascript
import { LineSeries, hideSeriesPaint } from '@pipsend/charts';

// The source (candles) keeps the real OHLC and everything attached to it.
const restore = hideSeriesPaint(source);
const view = chart.addSeries(LineSeries);
view.setData(bars.map(b => ({ time: b.time, value: b.close })));

// Back to candles:
chart.removeSeries(view);
restore();

hideSeriesPaint() paints the series transparent and turns off its last-price label, but leaves it visible to the engine, so its price lines and primitives stay on screen. It returns the function that puts the paint back. Push ticks to both series, because the view does not listen to the source. Line, Area and Baseline need { time, value }, not OHLC.

Volume under the price, same pane. A series with priceScaleId: '' lives on its own invisible overlay scale. Use scaleMargins to push it to the bottom:

javascript
const volume = chart.addSeries(HistogramSeries, {
    priceFormat: { type: 'volume' },
    priceScaleId: ''
});

chart.priceScale('', 0).applyOptions({
    scaleMargins: { top: 0.8, bottom: 0 }   // uses the lower 20%
});

Separate panes. The third argument of addSeries is the pane index:

javascript
const rsi = chart.addSeries(LineSeries, { color: '#7e57c2' }, 1);

The engine does not leave gaps between panes. If you ask for pane 3 and only panes 0 and 1 exist, the series lands in pane 2, and configuring the scale with the index you asked for throws incorrect pane index. Ask the chart where the series ended up:

javascript
const pane = rsi.getPane().paneIndex();
chart.priceScale('right', pane).applyOptions({ /* ... */ });

Pane API: chart.addPane(), chart.panes(), chart.removePane(i), series.moveToPane(i), and per pane getHeight(), setHeight(), setStretchFactor().

Two series on the same scale. Pass the same priceScaleId. To compare instruments with very different prices, use separate scales ('right' and 'left') or normalize the values.

Real-time updates

Use series.update(point) for live data. setData() re-ingests the whole series, which is noticeable on a long history.

javascript
// Update the last bar, or append a new one if the time is newer
series.update({ time: Math.floor(Date.now() / 1000), open: 100, high: 105, low: 95, close: 102 });

// Correct a bar that is already in the past
series.update(correctedBar, true);

Indicators and Heikin Ashi recalculate on their own after setData() or update(), and on each tick only the last point is rewritten.

When ticks arrive faster than you need to paint, batch them. This example from the library README keeps only the latest tick of each 100 ms window:

javascript
let updateQueue = [];
let updateTimer = null;

function queueUpdate(newCandle) {
    updateQueue.push(newCandle);

    if (!updateTimer) {
        updateTimer = setTimeout(() => {
            series.update(updateQueue[updateQueue.length - 1]);
            updateQueue = [];
            updateTimer = null;
        }, 100); // Batch updates every 100ms
    }
}

Related options:

  • timeScale.shiftVisibleRangeOnNewBar (default true) scrolls the view when a new bar arrives.
  • chart.timeScale().scrollToRealTime() jumps back to the latest bar.
  • A common error is Cannot update oldest data: update() was called with a time earlier than the last bar. Use update(point, true) for history.

Time scale and price scale

Time scale

Get it with chart.timeScale(). Options go through chart.applyOptions({ timeScale: { ... } }) or chart.timeScale().applyOptions({ ... }).

javascript
const timeScale = chart.timeScale();

timeScale.fitContent();                          // fit all data
timeScale.scrollToRealTime();                    // jump to the latest bar
timeScale.setVisibleRange({ from: startTime, to: endTime });
const range = timeScale.getVisibleRange();

timeScale.subscribeVisibleTimeRangeChange((range) => console.log(range));
Method Description
fitContent() Fit all data in view.
scrollToRealTime() Scroll to the latest bar.
scrollToPosition(position, animated), scrollPosition() Scroll by bars.
getVisibleRange() / setVisibleRange(range) Visible time range.
getVisibleLogicalRange() / setVisibleLogicalRange(range) Visible range by bar index.
resetTimeScale() Reset zoom and scroll.
timeToCoordinate(time), coordinateToTime(x), timeToIndex(time, findNearest?) Conversions.
subscribeVisibleTimeRangeChange, subscribeVisibleLogicalRangeChange, subscribeSizeChange Events (with unsubscribe* counterparts).

Time scale option defaults:

Option Default
rightOffset 0
barSpacing 6
minBarSpacing 0.5
maxBarSpacing 0 (no limit)
fixLeftEdge, fixRightEdge false
lockVisibleTimeRangeOnResize false
rightBarStaysOnScroll false
borderVisible, borderColor true, '#2B2B43'
visible true
timeVisible false (set true for intraday data)
secondsVisible true
shiftVisibleRangeOnNewBar true
ticksVisible false
uniformDistribution false
allowBoldLabels true

Time zone. localization.timeZone (an IANA zone such as 'America/New_York') shows the chart's times in that zone, daylight saving included. Your data stays in UTC. The crosshair label and default tick labels are written in local time, and a day starts at local midnight even when no bar opens exactly on it. The helper timeZoneOffset(zone, time) is exported so custom formatters can do the same.

javascript
chart.applyOptions({ localization: { timeZone: 'America/New_York' } });

Tick mark filter. timeScale.tickMarkFilter(time, tickMarkType, barsPerMark, previousTime) lets you decide which times may carry a mark or grid line on the axis. Times it rejects never get a mark; the rest are spread by weight and zoom as usual. barsPerMark says how many bars must separate two marks at the current zoom, so the filter can choose the finest regular grid that fits.

Fixed text at the chart edge. createTimeLabels(series, labels, options) draws a row of text labels, each centered on the bar at its time, a few pixels from the bottom (or top) edge of the pane. They do not depend on price, so they stay in place however you move or zoom the price scale (for example, a row of weekday names). Each label is { time, text, color?, size? }; options are position ('bottom' or 'top', default 'bottom'), offset (4), fontSize (11), fontFamily and color ('#787B86'). setLabels replaces them all at once. Times that are not on the time scale are not drawn.

Price scale

Get it with chart.priceScale('right') (or 'left', or an overlay ID), optionally with a pane index, or from a series with series.priceScale().

javascript
chart.priceScale('right').applyOptions({
    scaleMargins: { top: 0.1, bottom: 0.2 },
    borderColor: '#2B2B43',
});
Option Default
autoScale true
mode PriceScaleMode.Normal (also Logarithmic, Percentage, IndexedTo100)
invertScale false
alignLabels true
borderVisible, borderColor true, '#2B2B43'
scaleMargins { top: 0.2, bottom: 0.1 }
ticksVisible false
minimumWidth 0
ensureEdgeTickMarksVisible false
tickMarkSpacing 2.5

tickMarkSpacing is the room each axis label reserves, as a multiple of the font height. Lower it to show more prices on the axis. Steps are discrete, so values between 1.2 and 2.0 usually land on the same one; the real changes happen around 2.5, 1.5 and 1.0.

Price scale API: applyOptions(), options(), width(), setVisibleRange(), getVisibleRange(), setAutoScale(on).

Series-level conversions: series.priceToCoordinate(price) and series.coordinateToPrice(y). Price lines: series.createPriceLine(options), series.removePriceLine(line), series.priceLines().

Indicators

The library ships 23 indicators. All apply* functions share one signature, with the series first and the chart second:

javascript
apply<Name>(series, chart, options?)
javascript
import { createChart, CandlestickSeries, applySMA } from '@pipsend/charts';

const chart = createChart(container);
const candles = chart.addSeries(CandlestickSeries);
candles.setData(bars);

const sma = applySMA(candles, chart, { period: 20, color: '#2196F3' });

The indicator handle

Each apply* call returns a handle with the series it created plus four methods, identical for all indicators:

Method What it does
applyOptions(partial) Changes options on the fly. A color repaints; a period recalculates.
options() The current options, with defaults resolved.
recalculate() Forces a full recalculation.
remove() Removes the series and cuts the subscription to the source series.
javascript
sma.applyOptions({ period: 50, color: '#ff9800' });  // no destroy and recreate
sma.options().period;                                 // 50
sma.remove();

Call remove() when you are done with an indicator. Without it, the subscription outlives the indicator and keeps writing to series that no longer exist. Indicators recalculate by themselves when the source series changes; you do not need to call anything after setData() or update().

Series exposed by each indicator:

Indicator Exposed series
Bollinger, Keltner, Donchian upper, middle, lower
MACD macd, signal, histogram
Stochastic k, d
ADX adx, plusDI, minusDI
Ichimoku conversion, base, spanA, spanB, lagging
VWAP series, upper, lower
Pivot Points pivot, resistances[], supports[]
All the rest series

Shared options

Option Type Purpose
paneIndex number Pane to draw in. Fixed at creation: applyOptions ignores it.
lineWidth 1 | 2 | 3 | 4 Line width. Other values do nothing.
lineStyle 0..4 0 solid, 1 dotted, 2 dashed, 3 large dashed, 4 sparse dotted.
source IndicatorSource Which price of each bar is used.

source accepts 'close', 'open', 'high', 'low', 'hl2', 'hlc3' and 'ohlc4', and is supported by SMA, EMA, WMA, RSI and Keltner. On a line series, which only has value, any source resolves to that value.

Indicators that need volume

A price series does not carry volume, so feed it separately, once for all of them:

javascript
import { setSeriesVolumes } from '@pipsend/charts';

setSeriesVolumes(candles, bars);   // bars: [{ time, volume }, ...]

Volume, OBV, MFI and VWAP need this call and draw nothing without it. Volume Profile has its own channel, setVolumeProfileData().

Overlay indicators (on the price pane)

All default to paneIndex: 0.

SMA (Simple Moving Average)

javascript
applySMA(series, chart, { period: 20, color: '#2196F3' });
Option Default
period 20
color '#2196F3'
lineWidth 2
source 'close'
lineStyle 0

EMA (Exponential Moving Average)

javascript
applyEMA(series, chart, { period: 12, color: '#FF9800' });
Option Default
period 12
color '#FF9800'
lineWidth 2
source 'close'
lineStyle 0

WMA (Weighted Moving Average)

javascript
applyWMA(series, chart, { period: 20 });
Option Default
period 20
color '#4CAF50'
lineWidth 2
source 'close'
lineStyle 0

Bollinger Bands

javascript
const bb = applyBollingerBands(series, chart, { period: 20, stdDev: 2 });
bb.upper; bb.middle; bb.lower;
Option Default
period 20
stdDev 2
colorUpper '#3F51B5'
colorMiddle '#2196F3'
colorLower '#3F51B5'
lineWidth 1

Keltner Channels

javascript
const kc = applyKeltnerChannels(series, chart, { period: 20, multiplier: 2 });
Option Default
period 20
atrPeriod 10
multiplier 2
source 'close'
colorUpper, colorLower '#787B86'
colorMiddle '#2962FF'
lineWidth 1
lineStyle 0

Donchian Channels

javascript
const dc = applyDonchianChannels(series, chart, { period: 20 });
Option Default
period 20
colorUpper '#089981'
colorMiddle '#787B86'
colorLower '#F23645'
lineWidth 1
lineStyle 0
showMiddle true

Parabolic SAR

javascript
applyParabolicSAR(series, chart, { step: 0.02, maxStep: 0.2 });
Option Default
step 0.02
maxStep 0.2
colorUp '#089981'
colorDown '#F23645'
dotSize 2

SuperTrend

javascript
applySuperTrend(series, chart, { period: 10, multiplier: 3 });
Option Default
period 10
multiplier 3
colorUp '#089981'
colorDown '#F23645'
lineWidth 2

VWAP

Needs volume data.

javascript
const vwap = applyVWAP(series, chart, { anchorPeriod: 'daily', bandMultiplier: 1 });
vwap.series; vwap.upper; vwap.lower;
Option Default
anchorPeriod 'daily' (also 'weekly', 'monthly')
color '#2962FF'
lineWidth 2
lineStyle 0
bandMultiplier 0 (at 0, no bands are drawn)
bandColor '#787B86'

Ichimoku

The filled cloud and its projection beyond the last bar are drawn by a dedicated primitive, because the engine cannot do it natively.

javascript
const ich = applyIchimoku(series, chart);
ich.conversion; ich.base; ich.spanA; ich.spanB; ich.lagging;
Option Default
conversionPeriod 9
basePeriod 26
spanBPeriod 52
displacement 26
colorConversion '#2962FF'
colorBase '#F23645'
colorSpanA '#089981'
colorSpanB '#F23645'
colorLagging '#787B86'
cloudBullishColor '#089981'
cloudBearishColor '#F23645'
cloudOpacity 0.15
showCloud, showLagging true
lineWidth 1

Pivot Points

javascript
const pp = applyPivotPoints(series, chart, { pivotPeriod: 'daily', levels: 3 });
pp.pivot; pp.resistances; pp.supports;   // the last two are arrays
Option Default
pivotPeriod 'daily' (also 'weekly', 'monthly')
levels 3
colorPivot '#FF6D00'
colorResistance '#F23645'
colorSupport '#089981'
lineWidth 1
lineStyle 2

Panel indicators (separate panes)

Each one has its own default paneIndex. The engine leaves no gaps between panes, so a single indicator added alone lands in the first free pane, not necessarily at its default index (see Troubleshooting). Indicators resolve this internally. If you configure a scale yourself, ask where the series landed instead of assuming the requested index:

javascript
const pane = rsi.series.getPane().paneIndex();
chart.priceScale('right', pane).applyOptions({ /* ... */ });

RSI (default pane 1)

javascript
applyRSI(series, chart, { period: 14, overbought: 70, oversold: 30 });
Option Default
period 14
color '#9C27B0'
lineWidth 2
source 'close'
lineStyle 0
overbought 70
oversold 30
levelColor '#787B86'

The levels are drawn as dotted price lines on the pane.

MACD (default pane 2)

javascript
const macd = applyMACD(series, chart, { fastPeriod: 12, slowPeriod: 26, signalPeriod: 9 });
macd.macd; macd.signal; macd.histogram;
Option Default
fastPeriod 12
slowPeriod 26
signalPeriod 9
colorMACD '#2196F3'
colorSignal '#FF6D00'
colorHistogramUp '#26a69a'
colorHistogramDown '#ef5350'
lineWidth 2

Volume (default pane 3)

Needs volume data.

javascript
applyVolume(series, chart, { colorUp: '#26a69a', colorDown: '#ef5350' });
Option Default
colorUp '#26a69a'
colorDown '#ef5350'

Colors live on each bar, not on the series, so changing them triggers a recalculation rather than a repaint.

Stochastic (default pane 4)

javascript
const st = applyStochastic(series, chart, { kPeriod: 14, dPeriod: 3 });
st.k; st.d;
Option Default
kPeriod 14
dPeriod 3
colorK '#2196F3'
colorD '#FF6D00'
lineWidth 2
lineStyle 0
overbought 80
oversold 20
levelColor '#787B86'

OBV (default pane 5)

Needs volume data.

javascript
applyOBV(series, chart, { color: '#2196F3' });
Option Default
color '#2196F3'
lineWidth 2
data none (optional: your own volumes)

ATR (default pane 6)

javascript
applyATR(series, chart, { period: 14 });
Option Default
period 14
color '#FF9800'
lineWidth 2

ADX / DMI (default pane 7)

javascript
const adx = applyADX(series, chart, { period: 14, showDI: true });
adx.adx; adx.plusDI; adx.minusDI;
Option Default
period 14
colorADX '#F23645'
colorPlusDI '#089981'
colorMinusDI '#2962FF'
lineWidth 2
lineStyle 0
threshold 25
levelColor '#787B86'
showDI true

CCI (default pane 8)

javascript
applyCCI(series, chart, { period: 20 });
Option Default
period 20
color '#2962FF'
lineWidth 2
lineStyle 0
overbought 100
oversold -100
levelColor '#787B86'

Williams %R (default pane 9)

The scale runs from 0 to -100, so the levels are negative.

javascript
applyWilliamsR(series, chart, { period: 14 });
Option Default
period 14
color '#FF6D00'
lineWidth 2
lineStyle 0
overbought -20
oversold -80
levelColor '#787B86'

MFI (default pane 10)

Needs volume data.

javascript
applyMFI(series, chart, { period: 14 });
Option Default
period 14
color '#9C27B0'
lineWidth 2
lineStyle 0
overbought 80
oversold 20
levelColor '#787B86'

Primitive-based indicators

These two do not create series. They paint on the pane through a primitive, so their signature is different: the chart goes first.

javascript
create<Name>(chart, series, options?)

Trading Sessions (Forex)

Shades the Tokyo, London and New York sessions and, optionally, their overlaps. Each session runs on its own city's clock and follows its daylight saving.

javascript
import { createTradingSessionsIndicator, DEFAULT_SESSIONS } from '@pipsend/charts';

const sessions = createTradingSessionsIndicator(chart, series, {
    sessions: DEFAULT_SESSIONS,
    showOverlaps: true,
    enabled: true
});

sessions.applyOptions({ enabled: false });
sessions.remove();
Option Default
sessions DEFAULT_SESSIONS (see below)
showOverlaps true
overlapColor '#9C27B0'
overlapOpacity 0.15
enabled true (set false to switch it off without destroying it)

DEFAULT_SESSIONS in the source code:

Session Hours (local) Time zone Color
Tokyo 9 to 18 Asia/Tokyo #FF6B00
London 8 to 17 Europe/London #2962FF
New York 8 to 17 America/New_York #089981

Each TradingSession is { name, startHour, endHour, color, opacity, timeZone? }. A custom session without timeZone stays in UTC. Overlaps are computed bar by bar, because each session reads its own zone's clock.

Volume Profile

Distribution of volume by price level, with the Point of Control (POC) and the value area.

javascript
import { createVolumeProfileIndicator, setVolumeProfileData } from '@pipsend/charts';

const volumeProfile = createVolumeProfileIndicator(chart, series, {
    numberOfBins: 24,
    valueAreaPercentage: 0.7,
    showPOC: true,
    showValueArea: true
});

setVolumeProfileData(volumeProfile, chartData);   // chartData: [{ time, volume }, ...]
Option Default
numberOfBins 24
valueAreaPercentage 0.7 (70% of the volume)
showPOC true
showValueArea true
profileColor '#2962FF'
pocColor '#FF6B00'
valueAreaColor '#089981'
opacity 0.3
width 100 (pixels, capped at a fifth of the chart)
position 'right' (or 'left')

The option is named numberOfBins. There is no rowSize option.

Customizing indicator colors

javascript
applySMA(series, chart, {
    period: 20,
    color: '#FF6B6B',
    lineWidth: 3
});

applyRSI(series, chart, {
    period: 14,
    color: '#4ECDC4',
    lineWidth: 2
});

Drawing tools

Drawing tools are interactive objects painted on the chart canvas. Users can draw them with the mouse or touch, select them, drag their handles or bodies, and delete them. They are managed by a DrawingToolsManager.

Setting up the manager

javascript
import { DrawingToolsManager } from '@pipsend/charts';

const drawingManager = new DrawingToolsManager(chart, candlestickSeries, {
    enableKeyboardShortcuts: true,
    selectionColor: '#ff6b00',
    selectionLineWidth: 3
});

Manager options and defaults: enableKeyboardShortcuts true, selectionColor '#2962ff', selectionLineWidth 3.

Every tool is created with a factory that takes a context object { chart, series } and an options object. To let the user draw it, pass interactive: true, then register the tool with the manager:

javascript
import { createTrendLineTool } from '@pipsend/charts';

const trendLine = createTrendLineTool(
    { chart, series: candlestickSeries },
    {
        interactive: true,
        lineColor: '#2962FF',
        extendRight: true
    }
);
drawingManager.addTool(trendLine);   // returns the tool id

An interactive tool waits for the user's clicks (one, two or three, depending on the tool) and is placed where they click. Without interactive, pass explicit coordinates (time and price) and the tool is drawn immediately.

Manager API

Method Description
addTool(tool, id?) Register a tool. Returns its id.
removeTool(id) Remove one tool.
clearAll() Remove all tools.
getTool(id), getAllTools(), getToolCount() Look tools up.
startDrawing(type) Arm the chart: until stopDrawing(), a press places points of the tool being drawn, even on top of an existing drawing. Takes any tool name as a string.
stopDrawing(), activeDrawingType() Disarm / read the armed type.
hasPendingDrawing(), cancelPendingDrawings() Drawings still waiting for points.
subscribeDrawingCompleted(handler) / unsubscribeDrawingCompleted(handler) Called with the tool id when a drawing is finished.
selectTool(id), selectTools(ids), toggleToolSelection(id), deselectAll() Selection.
getSelectedToolId(), getSelectedToolIds(), isToolSelected(id) Read the selection.
deleteSelected() Remove every unlocked selected drawing, as one undo step.
subscribeSelectionChanged(handler) / unsubscribeSelectionChanged(handler) Receives { id, ids, bounds }.
selectionBounds() Pixel box of the selection, to anchor a floating toolbar.
undo(), redo(), canUndo(), canRedo(), recordChange() Undo history (up to 100 steps).
serializeAll(), restoreTool(context, data), restoreAll(context, tools, clearExisting?) See Serialization.
destroy() Detach all handlers and disarm.

Interaction and keyboard shortcuts

  • Draw by clicking or dragging. While a tool is armed and waiting for points, you can click point by point, or press, drag and release to place the first two points in one gesture (a trend line, rectangle or Fibonacci retracement can be drawn with a single drag). A three-point drawing takes two points from the drag and the third from a click. The mouse wheel and the axes keep working while armed, and touch still pans.
  • Select. Click a drawing to select it. Ctrl (or Cmd on macOS) plus click adds or removes a drawing from the selection. Ctrl plus a drag on empty space draws a box and adds every drawing it touches. Dragging any selected drawing moves all of them by the same offset, as one undo step.
  • Edit. Drag handles to change a drawing, or its body to move it. Handles appear on hover and respect the lock.
  • Keyboard (when enableKeyboardShortcuts is on and the focus is not in a text field):
Key Action
Delete / Backspace Delete the selected drawings.
Ctrl/Cmd + Z Undo.
Ctrl/Cmd + Shift + Z or Ctrl/Cmd + Y Redo.
Escape Disarm the tool, cancel pending drawings and deselect.
Double click or right click Finish an arrow in progress.

Common methods on every tool

All tools extend DrawingToolBase:

Method Description
remove() Remove the tool from the chart.
applyOptions(options) / options() Change or read options at runtime.
setColor(color) / color() Uniform color access.
show() / hide() Visibility.
lock() / unlock() / isLocked() Prevent editing.
serialize() Plain JSON-safe object (see Serialization).

Common options available on every tool: color, lineWidth, lineStyle ('solid' | 'dashed' | 'dotted'), visible, locked, zIndex, and hoverable.

Set hoverable: false for drawings nobody edits (for example, the output of a script that draws dozens of lines and boxes). They are left out of the hit test, so there is no hover, cursor or selection, and nothing is computed on pointer moves. Locked drawings stay hoverable by default, since a user may select one to unlock it.

Available tools

The following factories are exported. Each takes ({ chart, series }, options).

Category Tool Factory Type ID
Trading analysis Position createPositionTool position
Trading analysis Ruler createRulerTool ruler
Fibonacci Retracement createFibonacciTool fibRetracement
Fibonacci Extension createFibonacciExtensionTool fibExtension
Fibonacci Fan createFibonacciFanTool fibFan
Lines Trend line createTrendLineTool trendLine
Lines Horizontal line createHorizontalLineTool horizontalLine
Lines Vertical line createVerticalLineTool verticalLine
Lines Cross line createCrossLineTool crossLine
Lines Arrow createArrowTool arrow
Channels Parallel channel createParallelChannelTool parallelChannel
Channels Andrews' pitchfork createPitchforkTool pitchfork
Shapes Rectangle createRectangleTool rectangle
Shapes Circle / ellipse createCircleTool circle
Shapes Polygon (3 or more vertices) createPolygonTool polygon
Shapes Gantt / timeline createGanttTool gantt
Annotations Brush (freehand) createBrushTool brush
Annotations Text createTextTool text

Rays and extended lines are covered by options on the line tools: extendLeft and extendRight on the trend line, and startTime on the horizontal line (a horizontal ray).

Position tool

Marks a trade with entry, stop loss and take profit, and calculates risk/reward metrics. Pips are detected automatically from the number of decimals in the price (4 or more decimals: 0.0001, 3 decimals: 0.001, 2 decimals: 0.01, 0 or 1 decimal: 1). You can override this with pipConfig. The old symbol option was removed in 1.1.0 and is ignored.

javascript
import { createPositionTool } from '@pipsend/charts';

const position = createPositionTool(
    { chart, series: candlestickSeries },
    {
        entryPrice: 1.1000,
        entryTime: 1642000000,
        endTime: 1642086400,
        stopLoss: 1.0950,
        takeProfit: 1.1100,
        quantity: 1.0,
        onUpdate: (metrics) => {
            console.log('Risk (pips):', metrics.stopLossPips);
            console.log('Reward (pips):', metrics.takeProfitPips);
            console.log('R:R ratio:', metrics.riskRewardRatio);
        }
    }
);

Options: entryPrice and entryTime (required), endTime, stopLoss, takeProfit, quantity, pipConfig, interactive, profitColor, lossColor, entryLineColor, opacity, showTooltips, onUpdate, onRemove.

PositionMetrics: entryPrice, stopLoss, takeProfit, quantity?, stopLossPips, takeProfitPips, stopLossPercent, takeProfitPercent, riskRewardRatio, stopLossAmount?, takeProfitAmount?.

With interactive: true, the position waits for a click and is placed there, also to the right of the last bar. A position without endTime takes a fifth of the pane.

Ruler tool

Measures price distance, percentage change, pips, and number of bars between two points.

javascript
import { createRulerTool } from '@pipsend/charts';

const ruler = createRulerTool(
    { chart, series: candlestickSeries },
    {
        interactive: true,
        lineColor: '#2962FF',
        onUpdate: (metrics) => {
            console.log('Pips:', metrics.pips);
            console.log('Change %:', metrics.pricePercent);
            console.log('Bars:', metrics.barCount);
        }
    }
);

Options: point1Time, point1Price, point2Time, point2Price, symbol, pipConfig, interactive, measure ('both', 'price' or 'time', to measure along a single axis), lineColor, lineWidth, showLabel, labelBackgroundColor, labelTextColor, selectionColor, selectionLineWidth, onUpdate, onRemove.

RulerMetrics includes point1, point2, priceDifference, pricePercent, pips, timeDifference (seconds) and barCount.

Fibonacci retracement

javascript
import { createFibonacciTool } from '@pipsend/charts';

const fib = createFibonacciTool(
    { chart, series: candlestickSeries },
    {
        point1Time: startTime,
        point1Price: lowPrice,
        point2Time: endTime,
        point2Price: highPrice,
        levels: [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1],
        showLabels: true
    }
);

Options: point1Time, point1Price, point2Time, point2Price, useExtended, levels (plain ratios, which override useExtended), interactive, lineColor, lineWidth, showLabels, labelPosition ('left' or 'right'), levelColors (an object keyed by ratio as a string, for example "0.618"), onUpdate, onRemove.

Standard levels (used when levels is omitted): 0, 23.6%, 38.2%, 50%, 61.8%, 78.6%, 100%. With useExtended: true, the set also includes 127.2%, 141.4%, 161.8%, 200% and 261.8%. The ratio lists are exported as STANDARD_FIBONACCI_RATIOS and EXTENDED_FIBONACCI_RATIOS.

Fibonacci extension

A three-point projection for price targets. With interactive: true, click point A (swing low), point B (swing high), then point C (retracement level).

javascript
import { createFibonacciExtensionTool } from '@pipsend/charts';

const fibExt = createFibonacciExtensionTool(
    { chart, series: candlestickSeries },
    {
        interactive: true,
        showLabels: true,
        onUpdate: () => {
            const levels = fibExt.getExtensionLevels();
            console.log('Extension levels:', levels);
        }
    }
);

Options: point1, point2, point3 (each { time, price }), levels (an array of { level, color, label }), lineColor, lineWidth, showLabels, extendLines, interactive, onUpdate, onRemove. Default levels: 0, 61.8%, 100%, 161.8%, 261.8%.

Fibonacci fan

javascript
import { createFibonacciFanTool } from '@pipsend/charts';

const fibFan = createFibonacciFanTool(
    { chart, series: candlestickSeries },
    {
        interactive: true,
        showLabels: true,
        extendLines: true
    }
);

Options: point1Time, point1Price, point2Time, point2Price, levels ({ level, color, label }), lineWidth, showLabels, extendLines, interactive. Default fan levels: 38.2%, 50%, 61.8%.

Trend line

javascript
import { createTrendLineTool } from '@pipsend/charts';

const trendLine = createTrendLineTool(
    { chart, series: candlestickSeries },
    {
        interactive: true,
        lineColor: '#2962FF',
        lineStyle: 'solid',
        lineWidth: 2,
        extendRight: true,
        extendLeft: false
    }
);

Options: point1Time, point1Price, point2Time, point2Price, interactive, lineColor, lineWidth, lineStyle ('solid', 'dashed', 'dotted'), extendLeft, extendRight, showPriceLabel, onUpdate, onRemove.

Horizontal line

javascript
import { createHorizontalLineTool } from '@pipsend/charts';

const hLine = createHorizontalLineTool(
    { chart, series: candlestickSeries },
    {
        price: 1.1050,
        lineColor: '#2962FF',
        lineStyle: 'solid',
        lineWidth: 2,
        showLabel: true,
        labelText: 'Resistance'
    }
);

Options: price, interactive (waits for a click that sets the level), startTime (set it and the line becomes a horizontal ray from that time to the right edge; left undefined it spans the whole chart), lineColor, lineWidth, lineStyle, showLabel, labelText, showPriceLabel, onUpdate, onRemove. Drag to adjust the price.

Vertical line

javascript
import { createVerticalLineTool } from '@pipsend/charts';

const vLine = createVerticalLineTool(
    { chart, series: candlestickSeries },
    {
        time: eventTime,
        lineColor: '#787B86',
        lineStyle: 'dashed',
        lineWidth: 1,
        showLabel: true,
        labelText: 'News Event'
    }
);

Options: time, interactive, lineColor, lineWidth, lineStyle, showLabel, labelText, onUpdate, onRemove. Typical uses: economic events, news releases, session opens and closes.

Cross line

A horizontal and a vertical line crossing at one point. Options: time, price, interactive (waits for a click that sets both), lineColor, lineWidth, lineStyle, showPriceLabel, onUpdate, onRemove.

Arrow

Connects several points with lines and an arrow head on the last one.

javascript
import { createArrowTool } from '@pipsend/charts';

const arrow = createArrowTool(
    { chart, series: candlestickSeries },
    {
        interactive: true,
        lineColor: '#2962FF',
        lineWidth: 2,
        showArrowHead: true,
        showCircles: true,
        circleRadius: 4
    }
);

Options: points (an array of { time, price }), interactive, lineColor, lineWidth, showArrowHead, showCircles, circleRadius, onUpdate, onRemove, onComplete. Click to add points; finish with a double click or a right click.

Parallel channel

Options: point1Time, point1Price, point2Time, point2Price, channelWidth (distance to the parallel line, in price; negative sits below), interactive, lineColor, lineWidth, lineStyle, fillColor, fillOpacity, extendRight, showPriceLabel, onUpdate, onRemove, onComplete.

Andrews' pitchfork

Options: pivotTime, pivotPrice (the pivot the fork opens from), upperTime, upperPrice, lowerTime, lowerPrice (the two points whose midpoint the median line runs through), interactive, lineColor, lineWidth, lineStyle, fillColor, fillOpacity, onUpdate, onRemove, onComplete.

Rectangle

For order blocks, supply and demand zones, consolidation areas and price ranges.

javascript
import { createRectangleTool } from '@pipsend/charts';

const rectangle = createRectangleTool(
    { chart, series: candlestickSeries },
    {
        interactive: true,
        fillColor: '#2962FF',
        fillOpacity: 0.2,
        borderColor: '#2962FF',
        borderWidth: 2,
        borderStyle: 'solid',
        showLabel: true,
        labelText: 'Order Block',
        labelPosition: 'top-left'
    }
);

Options: time1, price1, time2, price2, interactive, fillColor, fillOpacity, borderColor, borderWidth, borderStyle, showLabel, labelText, labelPosition ('top-left', 'top-right', 'bottom-left', 'bottom-right', 'center', 'left', 'right'), labelColor, labelBackgroundColor, labelFontSize, onUpdate, onRemove. Click twice to set the corners, drag corners to resize, drag the body to move. A borderWidth of 0 draws no border.

Circle / ellipse

javascript
import { createCircleTool } from '@pipsend/charts';

const circle = createCircleTool(
    { chart, series: candlestickSeries },
    {
        interactive: true,
        fillColor: '#2962FF',
        fillOpacity: 0.1,
        borderColor: '#2962FF',
        borderWidth: 2,
        borderStyle: 'solid'
    }
);

Options: centerTime, centerPrice, radiusTime, radiusPrice, interactive, fillColor, fillOpacity, borderColor, borderWidth, borderStyle, showLabel, labelText, onUpdate, onRemove. Click to set the center and again to set the radius; the ellipse adapts to the price/time scale.

Polygon

Three or more vertices (so it also draws triangles). Options: points (an array of { time, price }), interactive, fillColor, fillOpacity, borderColor, borderWidth, borderStyle, onUpdate, onRemove, onComplete.

Gantt / timeline

Visualizes time ranges and trade planning across several rows.

javascript
import { createGanttTool } from '@pipsend/charts';

const gantt = createGanttTool(
    { chart, series: candlestickSeries },
    {
        segments: [
            {
                startTime: time1,
                endTime: time2,
                level: 0,
                label: 'Analysis Phase',
                color: '#2196F3',
                category: 'planning'
            },
            {
                startTime: time2,
                endTime: time3,
                level: 1,
                label: 'Entry Zone',
                color: '#4CAF50',
                category: 'trade'
            }
        ],
        rowHeight: 40,
        rowSpacing: 5,
        fillOpacity: 0.7,
        showLabels: true
    }
);

Options: segments ({ startTime, endTime, level, label, color, category? }), interactive, rowHeight, rowSpacing, fillOpacity, borderWidth, showLabels, showGrid, onUpdate, onRemove.

Brush

Freehand drawing with Catmull-Rom spline smoothing.

javascript
import { createBrushTool } from '@pipsend/charts';

const brush = createBrushTool(
    { chart, series: candlestickSeries },
    {
        color: '#2962FF',
        lineWidth: 2,
        smoothing: true,
        onComplete: () => {
            console.log('Brush stroke completed');
        }
    }
);

Options: points (an array of { time, price }), color, lineWidth, smoothing, sampleDistance, onUpdate, onRemove, onComplete. While the brush is armed, press and drag to draw as many strokes as you like; stroke samples are taken by distance and converted to time/price when the stroke ends. The brush only listens inside the drawing area, so the axes keep working. Each Ctrl/Cmd + Z removes one stroke.

Text

javascript
import { createTextTool } from '@pipsend/charts';

const text = createTextTool(
    { chart, series: candlestickSeries },
    {
        text: 'Important Level',
        fontSize: 14,
        textColor: '#FFFFFF',
        backgroundColor: '#2962FF',
        backgroundOpacity: 0.9,
        padding: 8,
        borderWidth: 1,
        textAlign: 'left',
        maxWidth: 200,
        time: annotationTime,
        price: annotationPrice
    }
);

Options: time, price, text, fontSize, fontFamily, textColor, backgroundColor, backgroundOpacity, padding, borderColor, borderWidth, textAlign ('left', 'center', 'right'), maxWidth, anchor ('top-left' by default, 'left', 'right', 'center': where the time/price point falls on the label), pointer (draws a tail toward the point), borderRadius, interactive, onUpdate, onRemove, onTextChange, onComplete. Text can be edited inline: type directly on the chart, press Enter or click outside to finish, press Escape to cancel. backgroundOpacity: 0 and padding: 0 are honored, so text without a background is possible.

Common style properties

Most tools use these names:

javascript
{
    // Line styles
    lineColor: '#2962FF',
    lineWidth: 2,
    lineStyle: 'solid' | 'dashed' | 'dotted',

    // Fill styles
    fillColor: '#2962FF',
    fillOpacity: 0.2,

    // Border styles
    borderColor: '#2962FF',
    borderWidth: 2,
    borderStyle: 'solid' | 'dashed' | 'dotted',

    // Labels
    showLabel: true,
    labelText: 'Custom Label',
    labelPosition: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center'
}

Drawing beyond the last bar

timeFromCoordinate() and coordinateFromTime() are exported helpers that extrapolate by logical index where the chart engine returns null, so drawings can be placed to the right of the last bar.

Best practices

  • Keep the number of active tools reasonable (fewer than 20 is recommended for best performance), and remove unused ones with tool.remove() or drawingManager.clearAll().
  • Keep references to the tools you create so you can update or remove them.
  • Show instructions for multi-step tools (for example, the three clicks of a Fibonacci extension).

Complete example

javascript
import {
    createChart,
    CandlestickSeries,
    DrawingToolsManager,
    createPositionTool,
    createTrendLineTool,
    createFibonacciTool,
    createRectangleTool,
    createTextTool
} from '@pipsend/charts';

const chart = createChart(document.getElementById('chart'), {
    width: 800,
    height: 600
});

const candlestickSeries = chart.addSeries(CandlestickSeries);
candlestickSeries.setData(data);

const drawingManager = new DrawingToolsManager(chart, candlestickSeries);

// Position tool
const position = createPositionTool(
    { chart, series: candlestickSeries },
    {
        entryPrice: 1.1000,
        entryTime: data[50].time,
        stopLoss: 1.0950,
        takeProfit: 1.1100,
        onUpdate: (metrics) => {
            console.log('R:R:', metrics.riskRewardRatio);
        }
    }
);
drawingManager.addTool(position);

// Trend line (interactive)
const trendLine = createTrendLineTool(
    { chart, series: candlestickSeries },
    { interactive: true, lineColor: '#2962FF', extendRight: true }
);
drawingManager.addTool(trendLine);

// Fibonacci retracement
const fib = createFibonacciTool(
    { chart, series: candlestickSeries },
    {
        point1Time: startTime,
        point1Price: 1.0850,
        point2Time: endTime,
        point2Price: 1.1100,
        showLabels: true
    }
);
drawingManager.addTool(fib);

// Order block rectangle
const orderBlock = createRectangleTool(
    { chart, series: candlestickSeries },
    {
        interactive: true,
        fillColor: '#089981',
        fillOpacity: 0.2,
        labelText: 'Demand Zone'
    }
);
drawingManager.addTool(orderBlock);

// Text annotation
const note = createTextTool(
    { chart, series: candlestickSeries },
    {
        text: 'Key Support Level',
        fontSize: 14,
        backgroundColor: '#2962FF',
        time: noteTime,
        price: notePrice
    }
);
drawingManager.addTool(note);

Trading features

Trading lines

createTradingLine(series, chart, options) creates a horizontal price line that the user can drag. Lines are draggable by default.

javascript
import {
    createTradingLine,
    createInteractiveLineManager,
    LineStyle
} from '@pipsend/charts';

// 1. Stop Loss with preset
const stopLoss = createTradingLine(series, chart, {
    price: 95.00,
    type: 'stop-loss',
    onDragStart: (price) => console.log('Drag started'),
    onDragMove: (price) => console.log('Moving:', price),
    onDragEnd: (price) => console.log('Final:', price)
});

// 2. Take Profit with preset
const takeProfit = createTradingLine(series, chart, {
    price: 110.00,
    type: 'take-profit',
    onDragEnd: (price) => updateOrderInBackend(price)
});

// 3. Entry line
const entry = createTradingLine(series, chart, {
    price: 100.00,
    type: 'entry'
});

// 4. Custom line with full control
const customLine = createTradingLine(series, chart, {
    price: 105.00,
    title: 'Target 1',
    color: '#FF9800',
    lineStyle: LineStyle.Dashed,
    lineWidth: 2,
    draggable: true,
    onDragEnd: (price) => console.log('Custom line:', price)
});

Preset types: 'stop-loss', 'take-profit', 'entry', 'target', 'resistance', 'support', 'custom'.

Preset Color Style Title
stop-loss #ef5350 Dashed Stop Loss
take-profit #26a69a Dashed Take Profit
entry #2196F3 Solid Entry
target #FF9800 Dashed Target
resistance #f44336 Solid Resistance
support #4CAF50 Solid Support
custom #9E9E9E Solid (empty)

All presets are draggable by default; user options override the preset.

Trading line options:

typescript
{
    price: number;                          // Required: line price
    type?: TradingLineType;                 // Preset (see above)
    title?: string;                         // Label text
    color?: string;                         // Line color
    lineWidth?: LineWidth;                  // Line width (1-4), default 2
    lineStyle?: LineStyle;                  // Solid, Dashed, etc.
    draggable?: boolean;                    // Enable dragging
    axisLabelVisible?: boolean;             // Show price on axis (default true)
    onDragStart?: (price: number) => void;  // Drag start callback
    onDragMove?: (price: number) => void;   // Drag move callback
    onDragEnd?: (price: number) => void;    // Drag end callback
    onClick?: (price: number) => void;      // Click without dragging (also for non-draggable lines)
    formatTitle?: (price: number) => string; // Dynamic title, e.g. a live P&L preview
}

Line methods:

javascript
const currentPrice = stopLoss.getPrice();   // Get current price
stopLoss.setPrice(93.00);                   // Update price programmatically
stopLoss.setTitle('Floating PnL: +12.50');  // Set the title immediately (bypasses formatTitle)
stopLoss.applyOptions({                     // Update options
    color: '#FF0000',
    title: 'New Stop Loss'
});
stopLoss.remove();                          // Remove line

onClick fires on a press and release without significant movement (under 4 px with a mouse, 12 px with a finger), so non-draggable lines can be made clickable (for example, "click an entry line to expand its SL/TP").

Click-to-create lines

createInteractiveLineManager(chart, series) arms the chart so the next click drops a line at the clicked price.

javascript
const manager = createInteractiveLineManager(chart, series);

document.getElementById('addStopLoss').onclick = async () => {
    const line = await manager.enableClickToCreate('stop-loss');
    // The promise resolves once the user has clicked on the chart
};

document.getElementById('addAlert').onclick = async () => {
    await manager.enableClickToCreate('custom', {
        title: 'Alert',
        color: '#FFC107',
        onDragEnd: (price) => api.createAlert(price)
    });
};

Modes: 'stop-loss', 'take-profit', 'entry', 'target', 'resistance', 'support', 'custom'. Other methods: disableClickToCreate(), getCreationMode(), isCreating().

Position lines

createPositionLine draws entry, take profit and stop loss as one unit: a label per level carrying a size badge and the P&L, a close button, and a connector tying the three together.

javascript
import { createPositionLine } from '@pipsend/charts';

const position = createPositionLine(series, chart, {
    entry: { price: 4306.79, badge: '1', formatText: () => '+ 2.32 USD' },
    takeProfit: { price: 4349.58, badge: '1', formatText: (p) => `+ ${(p - 4306.79).toFixed(2)} USD` },
    stopLoss: { price: 4266.06, badge: '1', formatText: (p) => `- ${(4306.79 - p).toFixed(2)} USD` },
    onDragEnd: (level, price) => updateOrderInBackend(level, price),
    onClose: (level) => cancelOrder(level)
});

position.setPrice('takeProfit', 4360);          // move a level
position.applyOptions({ stopLoss: undefined }); // drop a level
position.prices();                              // { entry, takeProfit }
position.remove();

Every level ('entry', 'takeProfit', 'stopLoss') is draggable and closable by default; set draggable: false or closable: false per level. Only the label takes the pointer.

Level options: price (required), text, badge, badgeFilled (default true for the entry, false otherwise), color, lineWidth (default 1), lineStyle (solid for the entry, dashed otherwise), draggable (true), closable (true), axisLabelVisible (true), formatText(price) (called on every price change, which keeps a live P&L in step with a drag). Default colors: entry #2962FF, take profit #26A17B, stop loss #D9A23B.

Group options: visible (true), connectorVisible (true), rightOffset (56), theme, and the callbacks onDragStart, onDragMove, onDragEnd, onDragCancel, onClose, onClick, each receiving (level, price) or (level).

The label look is shared through theme (background, textColor, badgeTextColor, borderRadius, borderWidth, paddingX, paddingY, fontSize, fontFamily, backgroundOpacity). The label background defaults to the chart's own background, so it reads on light and dark themes alike.

Behavior notes:

  • While a level is being dragged, it owns its price: applyOptions and setPrice do not move that level, so a feed that resends the old price several times a second cannot pull the line out from under the cursor. The other parts of the group keep updating.
  • onDragCancel reports a gesture that never confirmed (the pointer left the chart, Escape was pressed, or the line was removed). Together with onDragEnd, every drag that starts ends with a notification.
  • Labels that would overlap are fanned out so each can still be grabbed. All lines on the same chart (positions, pending orders, alerts) share the layout.
  • Touch is supported, with wider hit targets for a finger.

Markers

createSeriesMarkers(series, markers) places markers on bars.

javascript
import { createSeriesMarkers } from '@pipsend/charts';

const markers = createSeriesMarkers(series, [
    { time: '2024-01-03', position: 'belowBar', color: 'green', shape: 'arrowUp', text: 'BUY' }
]);

markers.setMarkers([]);   // clear
  • shape: 'circle', 'square', 'arrowUp', 'arrowDown', 'tag', 'xcross'.
  • position: 'aboveBar', 'belowBar', 'inBar', or at a price with 'atPriceTop', 'atPriceBottom', 'atPriceMiddle' (these need a price).
  • Other fields: id, text, size, textColor, borderColor, borderWidth, borderRadius.
  • The returned API has setMarkers(), markers() and detach().
  • createSeriesMarkers accepts hoverable: false for markers nobody edits.
  • xcross draws a thin cross, the usual mark of a cancelled or expired signal.

'tag' draws the text inside a rounded bubble with a tip pointing at the candle, the format of BUY/SELL signals on trading platforms. The other shapes draw the text beside the shape.

javascript
createSeriesMarkers(series, [
    {
        time: '2024-01-03',
        position: 'belowBar',
        shape: 'tag',
        color: '#26a69a',
        text: 'BUY',
        textColor: '#FFFFFF',   // white by default
        borderColor: '#0f3f38', // optional
        borderRadius: 6         // 6 by default
    }
]);

size scales the bubble and its tip like it scales the other shapes. Markers older than the first loaded bar are left out and appear on their own bars once the data reaches them.

Position markers (trade history)

createPositionMarkers shows closed and open trades as entry and exit markers.

javascript
import { createPositionMarkers } from '@pipsend/charts';

const positionMarkers = createPositionMarkers(candleSeries, [], {
    longColors: {
        entry: '#26a69a',
        exit: '#ef5350'
    },
    shortColors: {
        entry: '#ef5350',
        exit: '#26a69a'
    },
    defaultSize: 1.5
});

positionMarkers.addPosition({
    type: 'long',
    entry: { time: entryBar.time, price: entryBar.close },
    exit: { time: exitBar.time, price: exitBar.close },
    id: 'pos-1',
    text: { entry: 'BUY', exit: 'SELL' }
});

A TradingPosition has type ('long' or 'short'), entry, an optional exit (absent while the position is open), id, color, size, and text. The manager offers addPosition, addPositions, setPositions, closePosition(id, exit, exitText?), removePosition(id), clearPositions(), getPositions() and applyOptions(). Options: longColors, shortColors, defaultSize, entryOffset, exitOffset.

Orders and alerts

The library does not send orders. Trading lines and position lines are the visual and interactive layer: use the drag and close callbacks to update or cancel an order in your own backend.

javascript
const stopLoss = createTradingLine(series, chart, {
    price: currentPrice * 0.98,
    type: 'stop-loss',
    onDragEnd: async (price) => {
        await fetch('/api/orders/update', {
            method: 'POST',
            body: JSON.stringify({ stopLoss: price })
        });
    }
});

Serialization

All drawing tools support full serialization and deserialization. This enables:

  • Chart templates: save and load chart configurations to and from your backend.
  • Chart sync: synchronize chart state between devices, for example over a WebSocket.

Save and restore

typescript
import {
  DrawingToolsManager,
  SerializedDrawingTool,
  DrawingContext,
} from '@pipsend/charts';

// --- Serialize (save) ---
const serialized: SerializedDrawingTool[] = manager.serializeAll();
const json = JSON.stringify(serialized);
// Send `json` to backend or WebSocket

// --- Deserialize (restore) ---
const data: SerializedDrawingTool[] = JSON.parse(json);
const context: DrawingContext = { chart, series };
manager.restoreAll(context, data);

API

SerializedDrawingTool:

typescript
interface SerializedDrawingTool {
  type: string;
  points: Array<{ price: number; time: Time | null }>;
  style: Record<string, any>;
  extra?: Record<string, any>;
}
Field Description
type Tool type identifier (for example "brush", "trendLine", "fibRetracement").
points Coordinate points. Each has price (float) and time (Unix timestamp or null).
style All visual properties: colors, widths, opacities, etc.
extra Tool-specific data not covered by points and style (levels, segments, text content, etc.).

Methods:

  • tool.serialize(): SerializedDrawingTool (on every tool) returns a plain JSON-safe object with everything needed to recreate it. Newer versions also include the tool's id, visible and locked state.
  • manager.serializeAll(): SerializedDrawingTool[] serializes every managed tool.
  • manager.restoreTool(context, data): DrawingToolBase recreates one tool and adds it to the manager. Returns the new tool instance.
  • manager.restoreAll(context, tools, clearExisting?) restores an array. By default it clears existing tools first (clearExisting = true).
javascript
// Replace all tools with saved state
manager.restoreAll(context, savedTools);

// Append saved tools without clearing
manager.restoreAll(context, savedTools, false);

The helpers extractPoint, extractTime and makeSerializedPoint are also exported.

Type IDs

Type ID Tool Factory
trendLine Trend Line createTrendLineTool
horizontalLine Horizontal Line createHorizontalLineTool
verticalLine Vertical Line createVerticalLineTool
crossLine Cross Line createCrossLineTool
parallelChannel Parallel Channel createParallelChannelTool
pitchfork Pitchfork createPitchforkTool
polygon Polygon createPolygonTool
fibRetracement Fibonacci Retracement createFibonacciTool
fibExtension Fibonacci Extension createFibonacciExtensionTool
fibFan Fibonacci Fan createFibonacciFanTool
rectangle Rectangle createRectangleTool
circle Circle / Ellipse createCircleTool
arrow Arrow createArrowTool
brush Brush / Freehand createBrushTool
text Text Annotation createTextTool
ruler Ruler createRulerTool
position Position (Risk/Reward) createPositionTool
gantt Timeline / Gantt createGanttTool

Output examples

Trend line:

json
{
  "type": "trendLine",
  "points": [
    { "price": 1.08500, "time": 1711756800 },
    { "price": 1.09100, "time": 1711929600 }
  ],
  "style": {
    "lineColor": "#3B82F6",
    "lineWidth": 2,
    "lineStyle": "solid",
    "extendLeft": false,
    "extendRight": false
  }
}

Horizontal line (time is null because it spans the chart):

json
{
  "type": "horizontalLine",
  "points": [{ "price": 1.09000, "time": null }],
  "style": {
    "lineColor": "#8B5CF6",
    "lineWidth": 2,
    "lineStyle": "solid",
    "showLabel": true,
    "labelText": "Support"
  }
}

Fibonacci retracement:

json
{
  "type": "fibRetracement",
  "points": [
    { "price": 1.08750, "time": 1711843206 },
    { "price": 1.09200, "time": 1711929600 }
  ],
  "style": {
    "lineColor": "#787b86",
    "lineWidth": 1,
    "showLabels": true,
    "labelPosition": "right",
    "levelColors": { "0": "#787b86", "0.618": "#4caf50", "1": "#787b86" }
  },
  "extra": {
    "levels": [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1],
    "useExtended": false
  }
}

Rectangle:

json
{
  "type": "rectangle",
  "points": [
    { "price": 1.08500, "time": 1711756800 },
    { "price": 1.09100, "time": 1711929600 }
  ],
  "style": {
    "fillColor": "#2962FF",
    "fillOpacity": 0.2,
    "borderColor": "#2962FF",
    "borderWidth": 2,
    "borderStyle": "solid",
    "showLabel": true,
    "labelText": "Zone",
    "labelPosition": "top-left"
  }
}

Position:

json
{
  "type": "position",
  "points": [{ "price": 1.09000, "time": 1711843200 }],
  "style": {
    "profitColor": "#26a69a",
    "lossColor": "#ef5350",
    "entryLineColor": "#ffffff",
    "opacity": 0.3,
    "showTooltips": true
  },
  "extra": {
    "endTime": 1711929600,
    "stopLoss": 1.08500,
    "takeProfit": 1.09500,
    "quantity": 1.0
  }
}

Text annotation:

json
{
  "type": "text",
  "points": [{ "price": 1.08900, "time": 1711843200 }],
  "style": {
    "fontSize": 14,
    "fontFamily": "Arial, sans-serif",
    "textColor": "#FFFFFF",
    "backgroundColor": "#2962FF",
    "backgroundOpacity": 0.8,
    "padding": 8,
    "borderColor": "#2962FF",
    "borderWidth": 1,
    "textAlign": "left",
    "maxWidth": 200
  },
  "extra": { "text": "Key support level" }
}

Gantt:

json
{
  "type": "gantt",
  "points": [],
  "style": {
    "rowHeight": 40,
    "rowSpacing": 5,
    "fillOpacity": 0.7,
    "borderWidth": 2,
    "showLabels": true,
    "showGrid": true
  },
  "extra": {
    "segments": [
      {
        "startTime": 1711756800,
        "endTime": 1711929600,
        "level": 0,
        "label": "Phase 1",
        "color": "#2962FF",
        "category": "development"
      }
    ]
  }
}

The other tools follow the same structure: geometry in points, visual properties in style, and tool-specific data (Fibonacci levels, circle radius, ruler symbol, text content) in extra.

Chart templates (save and load)

typescript
async function saveTemplate(manager: DrawingToolsManager, name: string) {
  const drawings = manager.serializeAll();
  await fetch('/api/templates', {
    method: 'POST',
    body: JSON.stringify({ name, drawings, savedAt: Date.now() }),
  });
}

async function loadTemplate(manager: DrawingToolsManager, context: DrawingContext, id: string) {
  const res = await fetch(`/api/templates/${id}`);
  const { drawings } = await res.json();
  manager.restoreAll(context, drawings);
}

Chart sync (WebSocket)

typescript
// Sender
ws.send(JSON.stringify({ type: 'sync', drawings: manager.serializeAll() }));

// Receiver
ws.onmessage = (e) => {
  const msg = JSON.parse(e.data);
  if (msg.type === 'sync') manager.restoreAll(context, msg.drawings);
};

Notes

  • Callbacks are not serialized. Re-attach onUpdate, onRemove, etc. after a restore if you need them.
  • Interactive mode is not activated on restore. Tools come back in their completed state.
  • Tool IDs are regenerated on restore (unless the saved data carries one). Use the return value of restoreTool() to get the new instance.
  • time values are Unix timestamps in seconds, matching the chart's Time type. Horizontal lines use time: null.
  • restoreAll clears by default. Pass false as the third argument to append instead.
  • Data saved by versions before 2.4.0 cannot restore the segments of timelines or the levels of Fibonacci extensions and fans (they were saved under renamed keys). Drawings saved with 2.4.0 or later restore correctly.
  • Restored tools render correctly even on a chart whose candles differ from the ones they were drawn on: timestamps without an exact candle fall back to the nearest one.
  • Undo (Ctrl/Cmd + Z) restores drawings through the same path, so it benefits from the same fixes.

Theming and styling

Everything visual is a chart option, and options can be changed at runtime with chart.applyOptions().

Dark and light themes

javascript
// Dark Theme
const chart = createChart(container, {
    layout: {
        background: { color: '#1E222D' },
        textColor: '#D9D9D9',
    },
    grid: {
        vertLines: { color: '#2B2B43' },
        horzLines: { color: '#363C4E' },
    },
});

// Light Theme
const chart = createChart(container, {
    layout: {
        background: { color: '#FFFFFF' },
        textColor: '#191919',
    },
    grid: {
        vertLines: { color: '#e1e1e1' },
        horzLines: { color: '#e1e1e1' },
    },
});

Switching theme on a live chart:

javascript
chart.applyOptions({
    layout: { background: { color: '#0b0e14' }, textColor: '#d1d4dc' },
    grid: { vertLines: { color: '#151922' }, horzLines: { color: '#151922' } },
});

A full modern dark setup, from the library's trading UI example:

javascript
const chart = createChart(document.getElementById('chart'), {
    layout: {
        background: { type: 'solid', color: '#0b0e14' },
        textColor: '#d1d4dc',
        fontSize: 12,
    },
    grid: {
        vertLines: { color: '#151922' },
        horzLines: { color: '#151922' },
    },
    rightPriceScale: { borderColor: '#1e222d' },
    timeScale: { borderColor: '#1e222d', rightOffset: 12 },
    crosshair: { mode: 0 },
});

Series colors

javascript
const series = chart.addSeries(CandlestickSeries, {
    upColor: '#26a69a',
    downColor: '#ef5350',
    borderVisible: false,
    wickUpColor: '#26a69a',
    wickDownColor: '#ef5350',
});

Crosshair

crosshair.mode accepts CrosshairMode.Normal (moves freely), Magnet (sticks to the close price of OHLC series; the default), MagnetOHLC (sticks to the open, high, low or close), and Hidden. crosshair.vertLine and crosshair.horzLine each take color, width, style, visible, labelVisible and labelBackgroundColor.

Line styles

LineStyle values: Solid (0), Dotted (1), Dashed (2), LargeDashed (3), SparseDotted (4). Line widths for lines and indicators are 1 to 4.

Drawing tool and indicator colors

Every drawing tool takes color options (lineColor, fillColor, borderColor, ...) and supports tool.setColor(color). Every indicator takes its own color options and supports applyOptions({ color }) at runtime.

Responsive charts

javascript
window.addEventListener('resize', () => {
    chart.applyOptions({ width: window.innerWidth });
});

Or let the chart follow its container with autoSize: true.

Watermarks

createTextWatermark(pane, options) and createImageWatermark(pane, options) add a watermark to a pane.

Events and API reference

Chart events

javascript
chart.subscribeClick((param) => {
    console.log('Clicked at', param.time, param.point);
});

chart.subscribeCrosshairMove((param) => {
    if (!param.time) return;
    const bar = param.seriesData.get(series);
    console.log(param.time, bar);
});

The handler receives a MouseEventParams with: time, logical, point, paneIndex, seriesData (a Map of series to the bar under the cursor), hoveredSeries, hoveredObjectId, and sourceEvent. Unsubscribe with unsubscribeClick, unsubscribeDblClick and unsubscribeCrosshairMove.

Other events: timeScale().subscribeVisibleTimeRangeChange(...), subscribeVisibleLogicalRangeChange(...), subscribeSizeChange(...), and series.subscribeDataChanged(...).

Drawing manager events

javascript
drawingManager.subscribeSelectionChanged(({ id, ids, bounds }) => {
    // id is set only when exactly one drawing is selected; bounds boxes them all
});

drawingManager.subscribeDrawingCompleted((toolId) => {
    console.log('Finished drawing', toolId);
});

Exports at a glance

Area Exports
Chart createChart, createChartEx, version, timeFromCoordinate, coordinateFromTime, timeZoneOffset
Series definitions CandlestickSeries, BarSeries, LineSeries, AreaSeries, BaselineSeries, HistogramSeries
Enums LineStyle, LineType, CrosshairMode, PriceScaleMode, PriceLineSource, LastPriceAnimationMode, ColorType, TickMarkType, TrackingModeExitMode, MismatchDirection
Plugins createSeriesMarkers, createUpDownMarkers, createTimeLabels, createPositionMarkers, createTextWatermark, createImageWatermark
Indicators applySMA, applyEMA, applyWMA, applyBollingerBands, applyKeltnerChannels, applyDonchianChannels, applyParabolicSAR, applyVWAP, applySuperTrend, applyIchimoku, applyPivotPoints, applyRSI, applyMACD, applyStochastic, applyATR, applyVolume, applyOBV, applyADX, applyCCI, applyWilliamsR, applyMFI, createTradingSessionsIndicator, createVolumeProfileIndicator
Indicator helpers setSeriesVolumes, setVolumeProfileData, applyHeikinAshi, calculateHeikinAshi, hideSeriesPaint, DEFAULT_SESSIONS
Trading createTradingLine, createInteractiveLineManager, createPositionLine, DraggablePriceLine, InteractiveLineManager, PositionLine
Drawing DrawingToolsManager, DrawingToolBase, the 18 create*Tool factories, STANDARD_FIBONACCI_RATIOS, EXTENDED_FIBONACCI_RATIOS, getPipConfig, calculatePipConfig

The package ships TypeScript definitions for all of these (for example IChartApi, ISeriesApi, CandlestickData, SerializedDrawingTool, DrawingContext, the *Options types of each indicator and tool, and IndicatorRuntime).

Standalone build

When loaded from a script tag, every export above is available on window.PipsendCharts, for example PipsendCharts.createChart, PipsendCharts.CandlestickSeries, PipsendCharts.applySMA.

Framework integration

The library works with any framework: create the chart in the mount hook, update the series when data changes, and call chart.remove() when the component unmounts.

React

tsx
import { useEffect, useRef } from 'react';
import { createChart, CandlestickSeries, applySMA, applyRSI } from '@pipsend/charts';

function TradingChart({ data }) {
    const chartContainerRef = useRef<HTMLDivElement>(null);
    const chartRef = useRef<any>(null);
    const seriesRef = useRef<any>(null);

    useEffect(() => {
        if (!chartContainerRef.current) return;

        // Create chart
        chartRef.current = createChart(chartContainerRef.current, {
            width: chartContainerRef.current.clientWidth,
            height: 600
        });

        // Add series
        seriesRef.current = chartRef.current.addSeries(CandlestickSeries);
        seriesRef.current.setData(data);

        // Add indicators
        applySMA(seriesRef.current, chartRef.current, { period: 20 });
        applyRSI(seriesRef.current, chartRef.current, { period: 14 });

        // Cleanup
        return () => {
            if (chartRef.current) {
                chartRef.current.remove();
            }
        };
    }, []);

    // Update data when it changes
    useEffect(() => {
        if (seriesRef.current && data) {
            seriesRef.current.setData(data);
        }
    }, [data]);

    return <div ref={chartContainerRef} />;
}

Vue 3

vue
<template>
  <div ref="chartContainer"></div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue';
import { createChart, CandlestickSeries, applyMACD } from '@pipsend/charts';

const props = defineProps(['data']);
const chartContainer = ref(null);
let chart = null;
let series = null;

onMounted(() => {
    chart = createChart(chartContainer.value, {
        width: chartContainer.value.clientWidth,
        height: 600
    });

    series = chart.addSeries(CandlestickSeries);
    series.setData(props.data);

    // Add MACD
    applyMACD(series, chart, {
        fastPeriod: 12,
        slowPeriod: 26,
        signalPeriod: 9
    });
});

watch(() => props.data, (newData) => {
    if (series) {
        series.setData(newData);
    }
});

onUnmounted(() => {
    if (chart) {
        chart.remove();
    }
});
</script>

Angular

typescript
import { Component, ElementRef, Input, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { createChart, CandlestickSeries, applyBollingerBands } from '@pipsend/charts';

@Component({
  selector: 'app-trading-chart',
  template: '<div #chartContainer></div>'
})
export class TradingChartComponent implements OnInit, OnDestroy {
  @ViewChild('chartContainer') chartContainer!: ElementRef;
  @Input() data: any[];

  private chart: any;
  private series: any;

  ngOnInit() {
    this.chart = createChart(this.chartContainer.nativeElement, {
      width: this.chartContainer.nativeElement.clientWidth,
      height: 600
    });

    this.series = this.chart.addSeries(CandlestickSeries);
    this.series.setData(this.data);

    // Add Bollinger Bands
    applyBollingerBands(this.series, this.chart, {
      period: 20,
      stdDev: 2
    });
  }

  ngOnDestroy() {
    if (this.chart) {
      this.chart.remove();
    }
  }
}

Svelte

svelte
<script>
    import { onMount, onDestroy } from 'svelte';
    import { createChart, CandlestickSeries, applyStochastic } from '@pipsend/charts';

    export let data;

    let chartContainer;
    let chart;
    let series;

    onMount(() => {
        chart = createChart(chartContainer, {
            width: chartContainer.clientWidth,
            height: 600
        });

        series = chart.addSeries(CandlestickSeries);
        series.setData(data);

        // Add Stochastic
        applyStochastic(series, chart, {
            kPeriod: 14,
            dPeriod: 3
        });
    });

    $: if (series && data) {
        series.setData(data);
    }

    onDestroy(() => {
        if (chart) {
            chart.remove();
        }
    });
</script>

<div bind:this={chartContainer}></div>

Vanilla JavaScript

javascript
import {
    createChart,
    CandlestickSeries,
    applySMA,
    applyRSI,
    createTradingLine
} from '@pipsend/charts';

// Create chart
const chart = createChart(document.getElementById('chart'), {
    width: 800,
    height: 600
});

// Add candlestick series
const series = chart.addSeries(CandlestickSeries);
series.setData(historicalData);

// Add indicators
applySMA(series, chart, { period: 50, color: '#FF6D00' });
applyRSI(series, chart, { period: 14 });

// Add trading lines
const stopLoss = createTradingLine(series, chart, {
    price: 95.00,
    type: 'stop-loss',
    onDragEnd: (price) => {
        document.getElementById('slPrice').textContent = price.toFixed(2);
    }
});

Server-side rendering

The library needs a browser (Canvas API). In frameworks that render on the server, such as Next.js, load the chart component on the client only:

javascript
const Chart = dynamic(() => import('./Chart'), { ssr: false });

Examples

Full trading chart

javascript
import {
    createChart,
    CandlestickSeries,
    applySMA,
    applyEMA,
    applyBollingerBands,
    applyRSI,
    applyMACD,
    applyVolume,
    setSeriesVolumes,
    createTradingLine,
    createInteractiveLineManager
} from '@pipsend/charts';

// Fetch data from your API
const response = await fetch('/api/ohlcv');
const data = await response.json();

// Create chart
const chart = createChart(document.getElementById('chart'), {
    width: window.innerWidth,
    height: 600,
    layout: {
        background: { color: '#1E222D' },
        textColor: '#DDD',
    },
});

// Add candlestick series
const series = chart.addSeries(CandlestickSeries, {
    upColor: '#26a69a',
    downColor: '#ef5350',
});
series.setData(data);

// Add moving averages
applySMA(series, chart, { period: 20, color: '#2196F3' });
applyEMA(series, chart, { period: 50, color: '#FF9800' });

// Add Bollinger Bands
applyBollingerBands(series, chart, { period: 20, stdDev: 2 });

// Add oscillators
applyRSI(series, chart, { period: 14, color: '#9C27B0' });
applyMACD(series, chart, { fastPeriod: 12, slowPeriod: 26, signalPeriod: 9 });

// Add volume (data: [{ time, open, high, low, close, volume }, ...])
setSeriesVolumes(series, data);
applyVolume(series, chart, { colorUp: '#26a69a', colorDown: '#ef5350' });

// Add trading lines
const currentPrice = data[data.length - 1].close;

const stopLoss = createTradingLine(series, chart, {
    price: currentPrice * 0.98,
    type: 'stop-loss',
    onDragEnd: async (price) => {
        await fetch('/api/orders/update', {
            method: 'POST',
            body: JSON.stringify({ stopLoss: price })
        });
    }
});

const takeProfit = createTradingLine(series, chart, {
    price: currentPrice * 1.05,
    type: 'take-profit',
    onDragEnd: async (price) => {
        await fetch('/api/orders/update', {
            method: 'POST',
            body: JSON.stringify({ takeProfit: price })
        });
    }
});

// Interactive line manager
const manager = createInteractiveLineManager(chart, series);

document.getElementById('addSL').onclick = async () => {
    const line = await manager.enableClickToCreate('stop-loss', {
        onDragEnd: (price) => console.log('SL:', price)
    });
};

// Responsive
window.addEventListener('resize', () => {
    chart.applyOptions({ width: window.innerWidth });
});

Position lines with BUY/SELL tags

From the library's trading UI example:

javascript
const { createChart, CandlestickSeries, createSeriesMarkers, createPositionLine, LineStyle } = PipsendCharts;

const markers = createSeriesMarkers(series, [
    { time: candles[40].time, position: 'belowBar', shape: 'tag', color: '#26a69a', text: 'BUY' },
    { time: candles[92].time, position: 'aboveBar', shape: 'tag', color: '#ef5350', text: 'SELL' },
]);

const entryPrice = candles[candles.length - 1].close;

const position = createPositionLine(series, chart, {
    entry: {
        price: entryPrice,
        badge: '1',
        formatText: () => '+ 2.32 USD',
    },
    takeProfit: {
        price: entryPrice + 43,
        badge: '1',
        lineStyle: LineStyle.Solid,
        formatText: (price) => `+ ${((price - entryPrice) * 1).toFixed(2)} USD`,
    },
    stopLoss: {
        price: entryPrice - 41,
        badge: '1',
        lineStyle: LineStyle.Solid,
        formatText: (price) => `- ${Math.abs((price - entryPrice)).toFixed(2)} USD`,
    },
    onDragEnd: (level, price) => console.log(level, 'moved to', price.toFixed(2)),
    onClose: (level) => console.log(level, 'closed'),
    onClick: (level) => console.log(level, 'clicked'),
});

position.applyOptions({ connectorVisible: false });   // hide the connector

Indicators with a toggle

javascript
const indicators = { sma: null, rsi: null };

function toggleSMA() {
    if (indicators.sma) {
        indicators.sma.remove();
        indicators.sma = null;
    } else {
        indicators.sma = applySMA(series, chart, { period: 20, color: '#2962FF', lineWidth: 2 });
    }
}

function toggleRSI() {
    if (indicators.rsi) {
        indicators.rsi.remove();
        indicators.rsi = null;
    } else {
        indicators.rsi = applyRSI(series, chart, { period: 14, color: '#9C27B0' });
    }
}

When the last series of an indicator pane is removed, the pane closes automatically.

Sample pages

The repository ships HTML examples that load the standalone build from a script tag: a gallery (index.html), one page per series type (line, area, bar, baseline, histogram), indicators.html, drawing-tools-showcase.html, trading-lines.html, trading-lines-advanced.html, position-tool-example.html, position-markers-example.html and modern-trading-ui.html. They are available in the GitHub repository.

Troubleshooting

The chart does not render

The container is empty or the chart does not appear.

javascript
// 1. Ensure the container has explicit dimensions
const container = document.getElementById('chart');
container.style.width = '800px';
container.style.height = '600px';

// 2. Wait for the DOM to be ready
document.addEventListener('DOMContentLoaded', () => {
    const chart = createChart(container);
});

// 3. Check that the container exists
if (!container) {
    console.error('Chart container not found!');
}

If nothing is drawn, also check that time is in seconds (not milliseconds) and that the data is sorted in ascending order.

An indicator draws nothing

  • Arguments reversed. It is apply*(series, chart), not (chart, series). The two primitive-based indicators (createTradingSessionsIndicator, createVolumeProfileIndicator) are the other way around: (chart, series).
  • Volume, OBV, MFI or VWAP are empty. Call setSeriesVolumes(series, bars) first.
  • Volume Profile is empty. It uses setVolumeProfileData(), not setSeriesVolumes().
  • The period is longer than the data. With 10 candles, period: 20 cannot produce a value.
  • Data format. Times must be strings (YYYY-MM-DD) or timestamps, in a single form, ordered and unique.
  • lineWidth: 5 does nothing. Only 1, 2, 3 and 4 are accepted.

incorrect pane index

The engine does not leave gaps between panes. If you request pane 7 and only panes 0 and 1 exist, the series lands in pane 2, and configuring the scale of pane 7 throws this error. Ask the chart where the series ended up with series.getPane().paneIndex(). To move a series later, use series.moveToPane(i); applyOptions({ paneIndex }) on an indicator does not move it.

Ghost series after removing an indicator

You did not call remove() on the handle, so the subscription keeps writing. Always keep the handle and call remove().

Common data errors

Symptom Cause
Nothing is drawn time in milliseconds, or unsorted data
Cannot update oldest data update() with a time earlier than the last bar; use update(point, true)
Baseline all one color baseValue left at the default 0
Volume covers the price Missing priceScaleId: '' and scaleMargins
Histogram all one color The color goes on each point, not on the series options
The old series "comes back" after changing chart type A stored reference to an already removed series

Drawing tools

  • Tool not drawing. Verify the chart and series are initialized, that the tool was added to the manager, and that interactive: true is set for user-drawn tools.
  • Cannot select or drag a tool. Make sure the tool is added to the manager, that no other tool is currently armed, and that mouse events are not blocked by an overlay.
  • Tool disappears. Check that it was not removed, that its time and price are inside the visible range, and that you still hold a reference.
  • Performance with many drawings. Reduce the number of active tools, and set hoverable: false on drawings nobody edits.

TypeScript

Import types explicitly and annotate:

typescript
import {
    createChart,
    CandlestickSeries,
    IChartApi,
    ISeriesApi,
    CandlestickData
} from '@pipsend/charts';

const chart: IChartApi = createChart(container);
const series: ISeriesApi<"Candlestick"> = chart.addSeries(CandlestickSeries);

Performance

  • Limit the visible range with chart.timeScale().setVisibleRange({ from, to }) when working with very long histories.
  • Use series.update() for live data and batch bursts of ticks.
  • Add indicators only when needed, and remove those you no longer show.
  • Always ship the production build.

Module not found

Cannot find module '@pipsend/charts': reinstall the dependency and check it is listed in package.json.

bash
rm -rf node_modules package-lock.json
npm install
npm list @pipsend/charts

For TypeScript, make sure moduleResolution is set so the package exports map is understood (for example "node16", "nodenext" or "bundler").

Exporting an image

Use chart.takeScreenshot(), which returns a canvas:

javascript
const canvas = chart.takeScreenshot();
const image = canvas.toDataURL('image/png');

Getting help

Open an issue on GitHub with your code snippet, the expected and actual behavior, your browser and library version, and any error messages.

License

Pipsend Charts is distributed under a dual-license model. The package.json license field reads SEE LICENSE IN LICENSE. This section summarizes the license files shipped with the package; it is not legal advice, and the license files themselves are the authoritative text.

Free license. The LICENSE file grants permission, free of charge, to any person or organization obtaining a copy of the software to use it, subject to these conditions.

Permitted uses:

  • Use the software in personal projects (free).
  • Use the software in commercial applications (free).
  • Distribute the original, unmodified software.
  • Include the software as a dependency in your projects.
  • Use the software for educational purposes, and for research and development.

Restrictions:

  • You may not modify, alter, or create derivative works of the software for the purpose of commercial distribution or sale.
  • You may not sell, sublicense, or redistribute modified versions of the software for commercial purposes.
  • You may not remove or alter any copyright notices, trademarks, or attribution from the software.
  • You may not use the software's name, trademarks, or branding to endorse or promote derivative products without written permission.

Clarifications in the license: you can use the unmodified software in commercial applications for free, and you can make internal modifications for your own use (not for redistribution). You must include the license and copyright notice in all copies or substantial portions of the software.

Commercial license. A commercial license is required if you want to modify the software and distribute it commercially, create derivative works for commercial purposes, sell products based on modified versions, or redistribute modified versions under your own brand. The LICENSE-COMMERCIAL.md file describes the commercial offering, which it says includes full source code access, commercial redistribution rights, priority support, regular updates, and no attribution requirement ("Powered by Pipsend Charts" can be removed). To obtain one, contact licensing@pipsend.com.

Warranty. The software is provided "as is", without warranty of any kind, and the license limits liability as stated in the LICENSE file.

Third-party components. Pipsend Charts is based on TradingView's Lightweight Charts (copyright 2024 TradingView, Inc., Apache License 2.0) and uses fancy-canvas (ISC License). These components retain their original licenses.

Copyright notice:

text
Copyright (c) 2025 Pipsend
Based on TradingView's Lightweight Charts (Copyright 2024 TradingView, Inc.)