Add Technical Indicators to a Financial Chart
Technical indicators are studies (see Technical Indicators and Studies for the full list). This page shows how to:
- add technical indicators when the chart is created;
- add and remove them at runtime;
- build a technical indicator picker from the SDK's catalogue, with search, categories and favourites.
Each has TypeScript and React examples with live CodePens.
The SciTrader SDK has 32 built-in technical indicators and studies. For the full list, with each one's options and defaults, see Every Built-in Indicator and Study. The most popular indicators have a page of their own, listed under this page in the sidebar: what the indicator is, how it is calculated, how traders read it, and how to add it to a JavaScript or React financial chart.
Adding Technical Indicators When the Chart Is Created
Pass the studies in the studies option, in the order they should draw. The first is usually OHLCStudy, which draws the price series:
import {
BollingerBandsStudy, CandleInterval, CoinbaseExchange, OHLCStudy, RSIStudy,
SciFinanceChartPanel, SMAStudy, VolumeStudy,
} from "scichart-scitrader";
const financeChartPanel = await SciFinanceChartPanel.create(document.getElementById("chart"), {
exchangeName: CoinbaseExchange.name,
symbol: "BTC-USD",
interval: CandleInterval.SIX_HOURS,
period: "3M",
studies: [
new OHLCStudy(), // draws the price series
new VolumeStudy(),
// Overlays draw on the price pane
new SMAStudy({ length: 50, renderOptions: { stroke: "#ff9f1c", strokeThickness: 2 } }),
new BollingerBandsStudy({ period: 20, stdDev: 2 }),
// Oscillators each get a pane of their own
new RSIStudy({ length: 14, overbought: 70, oversold: 30 }),
],
});
- Overlays (moving averages, Bollinger Bands and other bands and channels, PSAR, VWAP, Ichimoku) share the price pane and its price axis.
- Oscillators (RSI, MACD, Stochastic, ADX, ATR and the rest) have their own scale, so the chart adds a pane for each one below the price pane. The price pane is three times the height of an indicator pane; to change that, use
paneSizing. - Constructor options set a study's inputs (lengths, levels) and styles (colours, thickness). Options left out take their defaults; the overview lists each study's options. The user can change all of them later in the Settings Form.
In React, pass the same studies in the config of SciFinanceChartView. Create them in a useMemo, once per mounted chart: a study is a live object, bound to one chart.
Adding and Removing Technical Indicators at Runtime
financeApi is the chart's SciFinanceChart. Its attachStudy adds a study, and detachStudy removes one; an oscillator's pane goes with it.
import { EStudyType, MACDStudy, studyProvider } from "scichart-scitrader";
const chart = financeChartPanel.financeApi;
// Create a study yourself...
const macd = new MACDStudy({ fast: 8, slow: 21 });
await chart.attachStudy(macd);
// ...or by type, with its default settings: what a study picker needs
const rsi = studyProvider.getStudy(EStudyType.RSI);
await chart.attachStudy(rsi);
chart.detachStudy(macd); // its pane goes with it
chart.studieslists the attached studies. It returns a new array each time, so in React, putting it in state re-renders.chart.studiesChangedfires on every attach and detach, including when the user closes a study's pane from the chart.studyProvider.getStudy(type)creates any registered study, built-in or your own, with its default settings. It is also how Save and Restore recreates studies.- The pane limit. The chart holds at most
maxPanespanes, 6 by default, counting the price pane. Past that,attachStudyrefuses an oscillator and raisespaneLimitReached, so you can tell the user. Overlays are never refused.
chart.studiesChanged.subscribe(() => console.log(chart.studies.map((study) => study.type)));
chart.paneLimitReached.subscribe(({ max, studyType }) => console.log(`${studyType} refused: at most ${max} panes`));
SciFinanceChart Study Methods and Events
| Member | What it does |
|---|---|
attach | Adds a study, and its pane if it needs one. Resolves when it has drawn |
detach | Removes a study, and its pane if nothing else is in it |
studies | The attached studies, in order |
studies | Fires after any attach or detach. No payload: read studies |
max | The most panes the chart holds, the price pane included. Default 6 |
pane | Fires when attach refuses a study because of max, with { |
SciFinanceChart.attachStudy, detachStudy, studies, studiesChanged, maxPanes and paneLimitReached, with their TSDoc
attachStudy(study: IStudy): Promise<void>;
detachStudy(study: IStudy): void;
get studies(): readonly IStudy[];
/**
* Fires whenever a study is attached or detached. Consumers (e.g. the studies-management
* drawer panel) subscribe to refresh their derived list. Carries no payload — subscribers
* read {@link studies} directly.
*/
studiesChanged: EventHandler<void>;
/**
* Maximum total number of chart panes (price pane + indicator panes).
* {@link attachStudy} refuses a study that would create a NEW pane once
* `chartPanes.size` has reached this value, raising {@link paneLimitReached}.
* Overlay studies that share an existing pane are never affected.
* Configurable via `ISciFinanceChartPredefinedStudiesOptions.maxPanes`.
*/
maxPanes: number;
/**
* Fires when {@link attachStudy} refuses a study because adding it would
* exceed {@link maxPanes}. Hosts subscribe to surface a "max panes reached"
* notification. Carries the cap and the refused study's type.
*/
paneLimitReached: EventHandler<{
max: number;
studyType: EStudyType;
}>;
The studyProvider Registry
studyProvider holds a factory for every registered study type. getStudy(type, descriptor?, options?) creates one, with the settings in descriptor if given (see Altering Study Settings), and returns undefined for an unknown type. getStudies() lists the registrations, including your own.
The StudyProvider class and the StudyRegistration type, with their TSDoc
declare class StudyProvider {
studies: Map<string, StudyRegistration>;
registerStudy(type: EStudyType | string, info: StudyRegistration): void;
getStudies(): StudyRegistration[];
getStudy(type: string, descriptor?: TStudyDescriptor, options?: IStudyOptions): IStudy | undefined;
}
const studyProvider: StudyProvider;
type StudyRegistration = {
type: EStudyType;
name: string;
description: string;
createStudy: (tempDescriptor?: TStudyDescriptor, options?: IStudyOptions) => IStudy;
};
TypeScript Example: Adding and Removing Technical Indicators at Runtime
A BTC-USD chart from Coinbase, created with Volume, a 50-candle SMA, Bollinger Bands and RSI. Pick an indicator from the dropdown, built with groupStudyMenuItems, and click Add indicator to add it; click a chip to remove that indicator. maxPanes is 5 here, so adding a fifth oscillator shows the pane-limit message.
The code for this example is found below.
- TS
import "scichart-scitrader/scitrader.css";
import {
BollingerBandsStudy,
CandleInterval,
CoinbaseExchange,
defaultExchangeProvider,
EStudyCategory,
EStudyType,
groupStudyMenuItems,
idbPriceStore,
IStudy,
OHLCStudy,
RSIStudy,
SciFinanceChartPanel,
SMAStudy,
STUDY_MENU_ITEMS,
studyProvider,
VolumeStudy,
} from "scichart-scitrader";
// This example expects this HTML:
// <div style="display: flex; flex-direction: column; height: 100%">
// <div id="toolbar"></div>
// <div id="chart" style="flex: 1; min-height: 0"></div>
// </div>
// Register a data source, once, at app startup
defaultExchangeProvider.registerExchange(CoinbaseExchange, idbPriceStore);
// The studies the picker offers: the SDK's catalogue, less the price study (always on this chart)
// and the order-book studies, which need an order-book source that CoinbaseExchange doesn't provide
const PICKABLE = STUDY_MENU_ITEMS.filter(
(item) => item.type !== EStudyType.OHLC && item.category !== EStudyCategory.OrderBook
);
// A study's display name, e.g. "Bollinger Bands", from the catalogue
const labelOf = (study: IStudy) => STUDY_MENU_ITEMS.find((item) => item.type === study.type)?.description ?? study.type;
async function createChart() {
const toolbar = document.getElementById("toolbar");
Object.assign(toolbar.style, toolbarStyle);
// 1. Add indicators when the chart is created: pass them in `studies`, in the order they draw.
// Overlays (SMA, Bollinger Bands) share the price pane; oscillators (RSI) get a pane each.
// Options left out take their defaults
const financeChartPanel = await SciFinanceChartPanel.create(document.getElementById("chart"), {
exchangeName: CoinbaseExchange.name, // name="Coinbase" matches the built-in CoinbaseExchange
symbol: "BTC-USD",
interval: CandleInterval.SIX_HOURS,
period: "3M",
studies: [
new OHLCStudy(), // draws the price series
new VolumeStudy(),
new SMAStudy({ length: 50, renderOptions: { stroke: "#ff9f1c", strokeThickness: 2 } }),
new BollingerBandsStudy({ period: 20, stdDev: 2 }),
new RSIStudy({ length: 14, overbought: 70, oversold: 30 }),
],
maxPanes: 5, // the price pane and up to four indicator panes
});
const chart = financeChartPanel.financeApi;
// 2. Add indicators at runtime: a picker of the catalogue, grouped by category.
// studyProvider.getStudy(type) creates any registered study with its default settings
const picker = document.createElement("select");
Object.assign(picker.style, controlStyle);
for (const group of groupStudyMenuItems(PICKABLE)) {
const optgroup = document.createElement("optgroup");
optgroup.label = group.category;
for (const item of group.items) {
optgroup.append(new Option(`${item.label}: ${item.description}`, item.type));
}
picker.append(optgroup);
}
const addButton = document.createElement("button");
addButton.textContent = "Add indicator";
Object.assign(addButton.style, controlStyle);
addButton.onclick = () => {
message.textContent = "";
chart.attachStudy(studyProvider.getStudy(picker.value));
};
// An oscillator needs a pane of its own. Past maxPanes, attachStudy refuses it and raises
// paneLimitReached; overlays are never refused
const message = document.createElement("span");
Object.assign(message.style, { color: "var(--st-ui-text-muted, #9da2bc)", fontSize: "13px" });
chart.paneLimitReached.subscribe(({ max }) => {
message.textContent = `At most ${max} panes: remove an indicator first`;
});
// 3. Remove indicators at runtime: one chip per study, with × calling detachStudy.
// An oscillator's pane goes with it
const chips = document.createElement("div");
Object.assign(chips.style, { display: "flex", flexWrap: "wrap", gap: "6px" });
const renderChips = () => {
chips.replaceChildren(
...chart.studies
.filter((study) => study.type !== EStudyType.OHLC)
.map((study) => {
const chip = document.createElement("button");
chip.textContent = `${labelOf(study)} ×`;
chip.title = "Remove";
Object.assign(chip.style, chipStyle);
chip.onclick = () => chart.detachStudy(study);
return chip;
})
);
};
// studiesChanged fires on every attach and detach, including a pane closed from the chart
chart.studiesChanged.subscribe(renderChips);
renderChips();
toolbar.append(picker, addButton, message, chips);
// When the page goes away: financeChartPanel.cleanup();
return financeChartPanel;
}
// The toolbar uses SciTrader's UI colours: its --st-ui-* CSS variables, with the stylesheet's defaults
const toolbarStyle: Partial<CSSStyleDeclaration> = {
display: "flex",
flexWrap: "wrap",
alignItems: "center",
gap: "8px",
padding: "8px",
background: "var(--st-ui-background, #2b2839)",
borderBottom: "1px solid var(--st-ui-divider, rgba(255,255,255,0.1))",
};
const controlStyle: Partial<CSSStyleDeclaration> = {
padding: "4px 8px",
border: "1px solid var(--st-ui-divider, rgba(255,255,255,0.1))",
borderRadius: "6px",
background: "var(--st-ui-surface, #373447)",
color: "var(--st-ui-text, #ffffff)",
font: "inherit",
fontSize: "13px",
};
const chipStyle: Partial<CSSStyleDeclaration> = {
...controlStyle,
borderRadius: "12px",
padding: "2px 10px",
cursor: "pointer",
};
createChart();
React Example: Adding and Removing Technical Indicators at Runtime
The same chart in React. The chips come from React state, which studiesChanged keeps up to date.
The code for this example is found below.
- React
import "scichart-scitrader/scitrader.css";
import * as React from "react";
import {
BollingerBandsStudy,
CandleInterval,
CoinbaseExchange,
defaultExchangeProvider,
EStudyCategory,
EStudyType,
groupStudyMenuItems,
idbPriceStore,
ISciFinanceChartPanelApi,
ISciFinanceChartPanelConfig,
IStudy,
OHLCStudy,
RSIStudy,
SMAStudy,
STUDY_MENU_ITEMS,
studyProvider,
VolumeStudy,
} from "scichart-scitrader";
import { SciFinanceChartView } from "scichart-scitrader-react";
// Register a data source, once, at app startup
defaultExchangeProvider.registerExchange(CoinbaseExchange, idbPriceStore);
// The studies the picker offers: the SDK's catalogue, less the price study (always on this chart)
// and the order-book studies, which need an order-book source that CoinbaseExchange doesn't provide
const PICKABLE = STUDY_MENU_ITEMS.filter(
(item) => item.type !== EStudyType.OHLC && item.category !== EStudyCategory.OrderBook
);
// A study's display name, e.g. "Bollinger Bands", from the catalogue
const labelOf = (study: IStudy) => STUDY_MENU_ITEMS.find((item) => item.type === study.type)?.description ?? study.type;
export function IndicatorsChart() {
// The same ISciFinanceChartPanelApi that SciFinanceChartPanel.create returns
const chartRef = React.useRef<ISciFinanceChartPanelApi>(null);
// The studies on the chart, kept in state so the chips re-render when they change
const [studies, setStudies] = React.useState<readonly IStudy[]>([]);
const [selected, setSelected] = React.useState<string>(EStudyType.MACD);
const [message, setMessage] = React.useState("");
// 1. Add indicators when the chart is created: pass them in `studies`, in the order they draw.
// Overlays (SMA, Bollinger Bands) share the price pane; oscillators (RSI) get a pane each.
// Create the config once per mounted chart: studies are live objects bound to one chart
const config = React.useMemo<ISciFinanceChartPanelConfig>(
() => ({
exchangeName: CoinbaseExchange.name, // name="Coinbase" matches the built-in CoinbaseExchange
symbol: "BTC-USD",
interval: CandleInterval.SIX_HOURS,
period: "3M",
studies: [
new OHLCStudy(), // draws the price series
new VolumeStudy(),
new SMAStudy({ length: 50, renderOptions: { stroke: "#ff9f1c", strokeThickness: 2 } }),
new BollingerBandsStudy({ period: 20, stdDev: 2 }),
new RSIStudy({ length: 14, overbought: 70, oversold: 30 }),
],
maxPanes: 5, // the price pane and up to four indicator panes
}),
[]
);
// financeApi throws until the chart has initialised, so subscribe from onInit.
// The chart is deleted when this component unmounts, so there's no need to unsubscribe
const onInit = (api: ISciFinanceChartPanelApi) => {
const chart = api.financeApi;
setStudies(chart.studies); // studies returns a new array each time, so React sees the change
// studiesChanged fires on every attach and detach, including a pane closed from the chart
chart.studiesChanged.subscribe(() => setStudies(chart.studies));
// Past maxPanes, attachStudy refuses an oscillator and raises paneLimitReached
chart.paneLimitReached.subscribe(({ max }) => setMessage(`At most ${max} panes: remove an indicator first`));
};
// 2. Add indicators at runtime: studyProvider.getStudy(type) creates any registered study
// with its default settings
const addStudy = () => {
const chart = chartRef.current;
if (!chart?.isInitialized) return;
setMessage("");
chart.financeApi.attachStudy(studyProvider.getStudy(selected));
};
// 3. Remove indicators at runtime: an oscillator's pane goes with it
const removeStudy = (study: IStudy) => chartRef.current?.financeApi.detachStudy(study);
// Fills its parent, so give the parent a height
return (
<div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
<div style={toolbarStyle}>
<select style={controlStyle} value={selected} onChange={(e) => setSelected(e.target.value)}>
{groupStudyMenuItems(PICKABLE).map((group) => (
<optgroup key={group.category} label={group.category}>
{group.items.map((item) => (
<option key={item.type} value={item.type}>
{`${item.label}: ${item.description}`}
</option>
))}
</optgroup>
))}
</select>
<button style={controlStyle} onClick={addStudy}>
Add indicator
</button>
<span style={{ color: "var(--st-ui-text-muted, #9da2bc)", fontSize: 13 }}>{message}</span>
<div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
{studies
.filter((study) => study.type !== EStudyType.OHLC)
.map((study) => (
<button key={study.id} style={chipStyle} title="Remove" onClick={() => removeStudy(study)}>
{`${labelOf(study)} ×`}
</button>
))}
</div>
</div>
<div style={{ flex: 1, minHeight: 0 }}>
<SciFinanceChartView ref={chartRef} config={config} onInit={onInit} />
</div>
</div>
);
}
// The toolbar uses SciTrader's UI colours: its --st-ui-* CSS variables, with the stylesheet's defaults
const toolbarStyle: React.CSSProperties = {
display: "flex",
flexWrap: "wrap",
alignItems: "center",
gap: 8,
padding: 8,
background: "var(--st-ui-background, #2b2839)",
borderBottom: "1px solid var(--st-ui-divider, rgba(255,255,255,0.1))",
};
const controlStyle: React.CSSProperties = {
padding: "4px 8px",
border: "1px solid var(--st-ui-divider, rgba(255,255,255,0.1))",
borderRadius: 6,
background: "var(--st-ui-surface, #373447)",
color: "var(--st-ui-text, #ffffff)",
font: "inherit",
fontSize: 13,
};
const chipStyle: React.CSSProperties = { ...controlStyle, borderRadius: 12, padding: "2px 10px", cursor: "pointer" };
Building a Technical Indicator Picker with STUDY_MENU_ITEMS
STUDY_MENU_ITEMS is the SDK's catalogue of the studies a picker can offer, in display order. Each item has a type to pass to studyProvider.getStudy, a short label such as "RSI", a description such as "Relative Strength Index", and a category. Two helpers do a picker's work without its UI:
filterStudyMenuItems(items, { query, category, isFavorite })narrows the list. The search matches labels, descriptions, categories and types, so"average","bollinger","volatility"and"rsi"all find what you'd expect.isFavoritekeeps only the studies it returnstruefor, e.g. a user's favourites.groupStudyMenuItems(items)sorts the list into categories, inSTUDY_CATEGORY_ORDER, A–Z within each.
import { filterStudyMenuItems, groupStudyMenuItems, STUDY_MENU_ITEMS } from "scichart-scitrader";
const matches = filterStudyMenuItems(STUDY_MENU_ITEMS, { query: "moving", category: "all" });
for (const { category, items } of groupStudyMenuItems(matches)) {
console.log(category, items.map((item) => item.label)); // "Trend" ["EMA", "SMA", "WMA"]
}
To list your own studies too, add an IStudyMenuItem for each to the list you filter. The examples below leave out the order-book studies, because the built-in Coinbase exchange has no order-book source.
The IStudyMenuItem interface, EStudyCategory enum and STUDY_MENU_ITEMS, with their TSDoc
/**
* Grouping shown in the Add Indicator pickers. Values double as the
* display labels.
*/
enum EStudyCategory {
Price = "Price & Chart Types",
Trend = "Trend",
Momentum = "Momentum",
Volatility = "Volatility",
Volume = "Volume",
OrderBook = "Order Book"
}
/** Category display order in the pickers. */
const STUDY_CATEGORY_ORDER: readonly EStudyCategory[];
/** One entry in {@link STUDY_MENU_ITEMS}. */
interface IStudyMenuItem {
type: EStudyType;
label: string;
description: string;
category: EStudyCategory;
disabled?: boolean;
}
/** Study types available in the Studies picker UI, in display order. */
const STUDY_MENU_ITEMS: IStudyMenuItem[];
filterStudyMenuItems, groupStudyMenuItems and the IStudyMenuFilter interface, with their TSDoc
/** A category to show, or every category. */
type TStudyCategoryFilter = EStudyCategory | "all";
/** Filter inputs for {@link filterStudyMenuItems}. */
interface IStudyMenuFilter {
/** Free-text search. Whitespace-separated terms must all match. */
query?: string;
category?: TStudyCategoryFilter;
/** When set, only items for which this returns true are kept (e.g. a favourites list). */
isFavorite?: (type: string) => boolean;
}
/** One category's studies, as {@link groupStudyMenuItems} returns them. */
interface IStudyMenuGroup {
category: EStudyCategory;
items: IStudyMenuItem[];
}
/**
* Narrow a study catalogue (`STUDY_MENU_ITEMS`) by category, search text and, optionally, a
* favourites test: the logic of an "add indicator" picker, without the UI.
*
* Search is case-insensitive and matches the label, description, category and study type, so
* "average", "bollinger", "volatility" and "rsi" all find what you'd expect. Every
* whitespace-separated term must match somewhere, so "moving exp" picks out EMA.
*/
function filterStudyMenuItems(items: readonly IStudyMenuItem[], { query, category, isFavorite }: IStudyMenuFilter): IStudyMenuItem[];
/**
* Sort studies into `STUDY_CATEGORY_ORDER` sections, A–Z by label within each. Empty sections are
* left out.
*/
function groupStudyMenuItems(items: readonly IStudyMenuItem[]): IStudyMenuGroup[];
TypeScript Example: A Technical Indicator Picker with Search, Categories and Favourites
A BTC-USD chart from Coinbase, with a technical indicator picker on the right:
- Search filters as you type: try "average", "volatility" or "oscillator".
- The category dropdown shows one category, or all of them.
- Favourites only shows the starred indicators. Click ☆ to star one.
- Click an indicator to add it to the chart. The picker shows how many of each are on the chart; close a pane to remove its indicator.
The code for this example is found below.
- TS
import "scichart-scitrader/scitrader.css";
import {
CandleInterval,
CoinbaseExchange,
defaultExchangeProvider,
EStudyCategory,
EStudyType,
filterStudyMenuItems,
groupStudyMenuItems,
idbPriceStore,
IStudyMenuItem,
OHLCStudy,
SciFinanceChartPanel,
STUDY_CATEGORY_ORDER,
STUDY_MENU_ITEMS,
studyProvider,
TStudyCategoryFilter,
VolumeStudy,
} from "scichart-scitrader";
// This example expects this HTML:
// <div style="display: flex; height: 100%">
// <div id="chart" style="flex: 1; min-width: 0"></div>
// <div id="picker" style="width: 300px; overflow-y: auto"></div>
// </div>
// Register a data source, once, at app startup
defaultExchangeProvider.registerExchange(CoinbaseExchange, idbPriceStore);
// The studies the picker offers: the SDK's catalogue, less the price study (always on this chart)
// and the order-book studies, which need an order-book source that CoinbaseExchange doesn't provide
const PICKABLE = STUDY_MENU_ITEMS.filter(
(item) => item.type !== EStudyType.OHLC && item.category !== EStudyCategory.OrderBook
);
const CATEGORIES = STUDY_CATEGORY_ORDER.filter((category) => category !== EStudyCategory.OrderBook);
async function createChart() {
const financeChartPanel = await SciFinanceChartPanel.create(document.getElementById("chart"), {
exchangeName: CoinbaseExchange.name, // name="Coinbase" matches the built-in CoinbaseExchange
symbol: "BTC-USD",
interval: CandleInterval.SIX_HOURS,
period: "3M",
studies: [new OHLCStudy(), new VolumeStudy()],
});
const chart = financeChartPanel.financeApi;
// The picker's state: the search text, the category shown, and the user's favourites
let query = "";
let category: TStudyCategoryFilter = "all";
let favouritesOnly = false;
const favourites = new Set<string>([EStudyType.RSI, EStudyType.MACD, EStudyType.BBANDS]);
const picker = document.getElementById("picker");
Object.assign(picker.style, panelStyle);
// Search box: filterStudyMenuItems matches labels, descriptions, categories and types,
// so "average", "bollinger", "volatility" and "rsi" all find what you'd expect
const search = document.createElement("input");
search.type = "search";
search.placeholder = "Search indicators";
Object.assign(search.style, inputStyle);
search.oninput = () => {
query = search.value;
render();
};
// Category filter: one EStudyCategory, or "all"
const categorySelect = document.createElement("select");
Object.assign(categorySelect.style, inputStyle);
categorySelect.append(new Option("All categories", "all"), ...CATEGORIES.map((c) => new Option(c, c)));
categorySelect.onchange = () => {
category = categorySelect.value as TStudyCategoryFilter;
render();
};
// Favourites only: isFavorite keeps just the studies it returns true for
const favouritesLabel = document.createElement("label");
const favouritesBox = document.createElement("input");
favouritesBox.type = "checkbox";
favouritesBox.onchange = () => {
favouritesOnly = favouritesBox.checked;
render();
};
favouritesLabel.append(favouritesBox, " Favourites only");
Object.assign(favouritesLabel.style, mutedStyle);
const message = document.createElement("div");
Object.assign(message.style, mutedStyle);
// Past maxPanes (6 by default), attachStudy refuses an oscillator and raises paneLimitReached
chart.paneLimitReached.subscribe(({ max }) => (message.textContent = `At most ${max} panes: close a pane first`));
const list = document.createElement("div");
picker.append(search, categorySelect, favouritesLabel, message, list);
// One row per study: click to add it to the chart, ☆ to favourite it
const row = (item: IStudyMenuItem) => {
const onChart = chart.studies.filter((study) => study.type === item.type).length;
const add = document.createElement("button");
Object.assign(add.style, rowStyle);
add.title = `Add ${item.description}`;
add.innerHTML = `<b>${item.label}</b>${onChart ? ` <small>(${onChart} on chart)</small>` : ""}<br><span style="opacity: 0.6">${item.description}</span>`;
add.onclick = () => {
message.textContent = "";
// studyProvider.getStudy creates any registered study by type, with its default settings
chart.attachStudy(studyProvider.getStudy(item.type));
};
const star = document.createElement("button");
star.textContent = favourites.has(item.type) ? "★" : "☆";
star.title = "Favourite";
Object.assign(star.style, starStyle);
star.onclick = () => {
favourites.has(item.type) ? favourites.delete(item.type) : favourites.add(item.type);
render();
};
const div = document.createElement("div");
Object.assign(div.style, { display: "flex", alignItems: "center" });
div.append(add, star);
return div;
};
function render() {
const items = filterStudyMenuItems(PICKABLE, {
query,
category,
isFavorite: favouritesOnly ? (type) => favourites.has(type) : undefined,
});
// groupStudyMenuItems: sections in STUDY_CATEGORY_ORDER, A–Z within each, empty ones left out
const groups = groupStudyMenuItems(items);
list.replaceChildren(
...groups.flatMap((group) => {
const heading = document.createElement("div");
heading.textContent = group.category;
Object.assign(heading.style, headingStyle);
return [heading, ...group.items.map(row)];
})
);
if (!groups.length) list.textContent = "No indicators match.";
}
// Re-render when studies are added or removed, to update the "on chart" counts
chart.studiesChanged.subscribe(render);
render();
// When the page goes away: financeChartPanel.cleanup();
return financeChartPanel;
}
// The picker uses SciTrader's UI colours: its --st-ui-* CSS variables, with the stylesheet's defaults
const panelStyle: Partial<CSSStyleDeclaration> = {
display: "flex",
flexDirection: "column",
gap: "8px",
padding: "12px",
boxSizing: "border-box",
background: "var(--st-ui-background, #2b2839)",
borderLeft: "1px solid var(--st-ui-divider, rgba(255,255,255,0.1))",
color: "var(--st-ui-text, #ffffff)",
fontSize: "13px",
};
const inputStyle: Partial<CSSStyleDeclaration> = {
padding: "6px 8px",
border: "1px solid var(--st-ui-divider, rgba(255,255,255,0.1))",
borderRadius: "6px",
background: "var(--st-ui-surface, #373447)",
color: "inherit",
font: "inherit",
};
const mutedStyle: Partial<CSSStyleDeclaration> = { color: "var(--st-ui-text-muted, #9da2bc)" };
const headingStyle: Partial<CSSStyleDeclaration> = {
...mutedStyle,
marginTop: "8px",
fontSize: "11px",
textTransform: "uppercase",
letterSpacing: "0.04em",
};
const rowStyle: Partial<CSSStyleDeclaration> = {
flex: "1",
padding: "6px 8px",
border: "0",
borderRadius: "6px",
background: "none",
color: "inherit",
font: "inherit",
textAlign: "left",
cursor: "pointer",
};
const starStyle: Partial<CSSStyleDeclaration> = {
border: "0",
background: "none",
color: "#ffb74d",
fontSize: "16px",
cursor: "pointer",
};
createChart();
React Example: A Technical Indicator Picker with Search, Categories and Favourites
The same picker in React. The search text, category, favourites and the chart's studies are React state, and the list is filtered and grouped on each render.
The code for this example is found below.
- React
import "scichart-scitrader/scitrader.css";
import * as React from "react";
import {
CandleInterval,
CoinbaseExchange,
defaultExchangeProvider,
EStudyCategory,
EStudyType,
filterStudyMenuItems,
groupStudyMenuItems,
idbPriceStore,
ISciFinanceChartPanelApi,
ISciFinanceChartPanelConfig,
IStudy,
OHLCStudy,
STUDY_CATEGORY_ORDER,
STUDY_MENU_ITEMS,
studyProvider,
TStudyCategoryFilter,
VolumeStudy,
} from "scichart-scitrader";
import { SciFinanceChartView } from "scichart-scitrader-react";
// Register a data source, once, at app startup
defaultExchangeProvider.registerExchange(CoinbaseExchange, idbPriceStore);
// The studies the picker offers: the SDK's catalogue, less the price study (always on this chart)
// and the order-book studies, which need an order-book source that CoinbaseExchange doesn't provide
const PICKABLE = STUDY_MENU_ITEMS.filter(
(item) => item.type !== EStudyType.OHLC && item.category !== EStudyCategory.OrderBook
);
const CATEGORIES = STUDY_CATEGORY_ORDER.filter((category) => category !== EStudyCategory.OrderBook);
export function IndicatorPickerChart() {
const chartRef = React.useRef<ISciFinanceChartPanelApi>(null);
// The studies on the chart, kept in state for the "on chart" counts
const [studies, setStudies] = React.useState<readonly IStudy[]>([]);
const [message, setMessage] = React.useState("");
// The picker's state: the search text, the category shown, and the user's favourites
const [query, setQuery] = React.useState("");
const [category, setCategory] = React.useState<TStudyCategoryFilter>("all");
const [favouritesOnly, setFavouritesOnly] = React.useState(false);
const [favourites, setFavourites] = React.useState<ReadonlySet<string>>(
new Set([EStudyType.RSI, EStudyType.MACD, EStudyType.BBANDS])
);
// Create the config once per mounted chart: studies are live objects bound to one chart
const config = React.useMemo<ISciFinanceChartPanelConfig>(
() => ({
exchangeName: CoinbaseExchange.name, // name="Coinbase" matches the built-in CoinbaseExchange
symbol: "BTC-USD",
interval: CandleInterval.SIX_HOURS,
period: "3M",
studies: [new OHLCStudy(), new VolumeStudy()],
}),
[]
);
// financeApi throws until the chart has initialised, so subscribe from onInit.
// The chart is deleted when this component unmounts, so there's no need to unsubscribe
const onInit = (api: ISciFinanceChartPanelApi) => {
const chart = api.financeApi;
setStudies(chart.studies);
chart.studiesChanged.subscribe(() => setStudies(chart.studies));
// Past maxPanes (6 by default), attachStudy refuses an oscillator and raises paneLimitReached
chart.paneLimitReached.subscribe(({ max }) => setMessage(`At most ${max} panes: close a pane first`));
};
// studyProvider.getStudy creates any registered study by type, with its default settings
const addStudy = (type: string) => {
if (!chartRef.current?.isInitialized) return;
setMessage("");
chartRef.current.financeApi.attachStudy(studyProvider.getStudy(type));
};
const toggleFavourite = (type: string) =>
setFavourites((previous) => {
const next = new Set(previous);
next.has(type) ? next.delete(type) : next.add(type);
return next;
});
// filterStudyMenuItems matches the search against labels, descriptions, categories and types,
// keeps one category (or "all"), and with isFavorite, only the favourites.
// groupStudyMenuItems sorts the result into category sections, A–Z within each
const groups = groupStudyMenuItems(
filterStudyMenuItems(PICKABLE, {
query,
category,
isFavorite: favouritesOnly ? (type) => favourites.has(type) : undefined,
})
);
// Fills its parent, so give the parent a height
return (
<div style={{ display: "flex", height: "100%" }}>
<div style={{ flex: 1, minWidth: 0 }}>
<SciFinanceChartView ref={chartRef} config={config} onInit={onInit} />
</div>
<div style={panelStyle}>
<input
type="search"
placeholder="Search indicators"
style={inputStyle}
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<select
style={inputStyle}
value={category}
onChange={(e) => setCategory(e.target.value as TStudyCategoryFilter)}
>
<option value="all">All categories</option>
{CATEGORIES.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
<label style={mutedStyle}>
<input type="checkbox" checked={favouritesOnly} onChange={(e) => setFavouritesOnly(e.target.checked)} />{" "}
Favourites only
</label>
<div style={mutedStyle}>{message}</div>
{groups.length === 0 && <div>No indicators match.</div>}
{groups.map((group) => (
<React.Fragment key={group.category}>
<div style={headingStyle}>{group.category}</div>
{group.items.map((item) => {
const onChart = studies.filter((study) => study.type === item.type).length;
return (
<div key={item.type} style={{ display: "flex", alignItems: "center" }}>
<button style={rowStyle} title={`Add ${item.description}`} onClick={() => addStudy(item.type)}>
<b>{item.label}</b>
{onChart > 0 && <small>{` (${onChart} on chart)`}</small>}
<br />
<span style={{ opacity: 0.6 }}>{item.description}</span>
</button>
<button style={starStyle} title="Favourite" onClick={() => toggleFavourite(item.type)}>
{favourites.has(item.type) ? "★" : "☆"}
</button>
</div>
);
})}
</React.Fragment>
))}
</div>
</div>
);
}
// The picker uses SciTrader's UI colours: its --st-ui-* CSS variables, with the stylesheet's defaults
const panelStyle: React.CSSProperties = {
display: "flex",
flexDirection: "column",
gap: 8,
width: 300,
padding: 12,
boxSizing: "border-box",
overflowY: "auto",
background: "var(--st-ui-background, #2b2839)",
borderLeft: "1px solid var(--st-ui-divider, rgba(255,255,255,0.1))",
color: "var(--st-ui-text, #ffffff)",
fontSize: 13,
};
const inputStyle: React.CSSProperties = {
padding: "6px 8px",
border: "1px solid var(--st-ui-divider, rgba(255,255,255,0.1))",
borderRadius: 6,
background: "var(--st-ui-surface, #373447)",
color: "inherit",
font: "inherit",
};
const mutedStyle: React.CSSProperties = { color: "var(--st-ui-text-muted, #9da2bc)" };
const headingStyle: React.CSSProperties = {
...mutedStyle,
marginTop: 8,
fontSize: 11,
textTransform: "uppercase",
letterSpacing: "0.04em",
};
const rowStyle: React.CSSProperties = {
flex: 1,
padding: "6px 8px",
border: 0,
borderRadius: 6,
background: "none",
color: "inherit",
font: "inherit",
textAlign: "left",
cursor: "pointer",
};
const starStyle: React.CSSProperties = { border: 0, background: "none", color: "#ffb74d", fontSize: 16, cursor: "pointer" };
Where to read next
- Technical Indicators and Studies: every indicator, with its options.
- Altering Study Settings: let users change an indicator's settings.
- Price Chart Types: candlestick, Heikin-Ashi, Renko and Point & Figure charts.
- Creating Custom Studies: add an indicator of your own to the picker.