Skip to main content

Mobile and Desktop Modes

A chart works by touch out of the box: pinch to zoom, drag to pan, double-tap to fit. But some parts are laid out for a mouse, around hover and small targets. For phones and tablets, each has a touch mode, which your app turns on.

The chart doesn't detect touch screens: your app decides. This is by design. Your app already chooses between its phone and desktop layouts, e.g. with a breakpoint hook, and the chart should match it: a tablet with a keyboard and trackpad may want either. So decide once, in your app, and pass the result to the chart.

What Changes in Touch Mode​

Mouse (desktop)Touch (phones and tablets)Set with
Zoom and panWheel zooms, drag pans, double-click fitsPinch zooms, drag pans, double-tap fitsNothing: both always work
Pane control barsAppear over a pane on hoverIn a drawer at the pane's right edge: tap or swipe its handlepaneControls: { touch }
Dividers between panesThinA wider grip, with dotspaneControls: { touch }
Selecting a drawingClick itLong-press itDrawing tools' touch
Placing a drawingClick each pointDrag a cursor to aim, tap to place each pointDrawing tools' touch
A finished drawingStays selected, with its quick-edit toolbarNot selected, so its editor doesn't cover the chartselectFinishedDrawing
Crosshair and legendOn: they follow the mouseOff by default; while on, a drag moves the crosshair instead of panningYour crosshair button
On-chart zoom buttonsOptionalLeave them out: small targets that fight the pan gestureOnChartButtonsModifier

Choosing Touch or Mouse in Your App​

A media query is the simplest test: (pointer: coarse) matches when the main pointer is a finger.

const touch = matchMedia("(pointer: coarse)").matches;

The SciTrader app uses a breakpoint hook, useBreakpoint, which gives "desktop" only for a fine pointer (a mouse or trackpad) and a window at least 1024 px wide. Everything else gets the touch layout, tablets in landscape included:

// "desktop" needs a mouse or trackpad and room; phones and tablets get the touch layout
function useTouchLayout(): boolean {
const query = "(pointer: fine) and (min-width: 1024px)";
const isDesktop = React.useSyncExternalStore(
(onChange) => {
const media = matchMedia(query);
media.addEventListener("change", onChange);
return () => media.removeEventListener("change", onChange);
},
() => matchMedia(query).matches,
() => true // server render: assume desktop
);
return !isDesktop;
}

Server rendering. The server can't know the device, so it renders one layout, here desktop, and the browser corrects it after hydration. Create the chart after that, so it starts in the right mode: the SciTrader app waits one effect before mounting the chart.

Creating a Chart in Touch or Mouse Mode​

Pass the mode to each part that has one:

const financeChartPanel = await SciFinanceChartPanel.create(element, {
exchangeName: CoinbaseExchange.name,
symbol: "BTC-USD",
interval: CandleInterval.ONE_HOUR,
studies: [new OHLCStudy(), new VolumeStudy(), new RSIStudy()],
paneControls: { touch }, // control bars in drawers, wider dividers
});

const drawing = DrawingToolsController.attach(financeChartPanel.financeApi, {
touch, // long press selects; placement with a cursor
selectFinishedDrawing: !touch, // on a phone, an open editor would cover the drawing
});

In React, pass paneControls={{ touch }} to <SciFinanceChartView>, and the same options to useDrawingTools.

Switching Mode​

  • The pane controls switch live: financeChartPanel.setPaneControls({ touch }), or a new paneControls prop in React.
  • The drawing tools' options are read when they attach. To switch them, create the chart again in the new mode: in React, give <SciFinanceChartView> a key that changes with the mode. Save the chart with toJSON() first if you want to keep its studies and drawings; see Saving and Restoring.

A switch is rare, e.g. a tablet's keyboard and trackpad being attached. The SciTrader app avoids switching a live chart: it waits until it knows its layout before creating the chart, so the chart starts in the right mode.

The Crosshair on Touch Screens​

With a mouse, hovering moves the crosshair and dragging pans, so both work at once. A finger can't hover: one drag has to do one or the other. So on touch, the SciTrader app:

  1. starts with the crosshair and legend off, so a drag pans;
  2. has a crosshair button. While the crosshair is on, a drag moves it, and pan, X-axis drag and double-tap zoom-to-fit are off.

FinChartLegendModifier draws the crosshair and the legend, and setCursorEnabled turns both on or off. Turning the pan modifiers off is up to you:

import { XAxisDragModifier, ZoomPanModifier } from "scichart";
import { FinChartLegendModifier, FinChartZoomExtentsModifier, SciFinanceChart } from "scichart-scitrader";

function setCrosshair(chart: SciFinanceChart, on: boolean, touch: boolean) {
chart.finChartModifiers
.asArray()
.find((m): m is FinChartLegendModifier => m instanceof FinChartLegendModifier)
?.setCursorEnabled(on);
if (touch) setPanAndZoom(chart, !on); // on touch, the crosshair takes the drag
}

function setPanAndZoom(chart: SciFinanceChart, enabled: boolean) {
chart.chartPanes.forEach(({ surface }) =>
surface.chartModifiers.asArray().forEach((m) => {
if (m instanceof ZoomPanModifier || m instanceof XAxisDragModifier || m instanceof FinChartZoomExtentsModifier) {
m.isEnabled = enabled;
}
})
);
}

The drawing tools turn panning off while a tool is in use, and back on when it finishes. With the crosshair on, it should stay off, so pass resumeGestures to the drawing tools: resumeGestures: () => setPanAndZoom(chart, !(touch && crosshairOn)).

TypeScript Example: Touch and Mouse Modes​

BTC-USD from Coinbase, with the drawing tools rail. Auto picks touch from (pointer: coarse); Touch and Mouse override it, so you can try touch mode with a mouse. In touch mode, each pane’s control bar is behind the handle at its right edge, the divider has a grip, the crosshair starts off, and drawing tools place points with a cursor. Turn the crosshair on, and a drag moves it instead of panning. A mode change creates the chart again.

The code for this example is found below.

import "scichart-scitrader/scitrader.css";
import { XAxisDragModifier, ZoomPanModifier } from "scichart";
import {
CandleInterval,
CoinbaseExchange,
defaultExchangeProvider,
DrawingToolbar,
DrawingToolsController,
FinChartLegendModifier,
FinChartZoomExtentsModifier,
idbPriceStore,
ISciFinanceChartPanelApi,
OHLCStudy,
RSIStudy,
SciFinanceChart,
SciFinanceChartPanel,
VolumeStudy,
} from "scichart-scitrader";

// This example expects this HTML:
// <div style="display: flex; flex-direction: column; height: 100%">
// <div id="toolbar"></div>
// <div style="display: flex; flex: 1; min-height: 0">
// <div id="tools"></div>
// <div id="chart" style="flex: 1; min-width: 0"></div>
// </div>
// </div>

// Register a data source, once, at app startup
defaultExchangeProvider.registerExchange(CoinbaseExchange, idbPriceStore);

// 1. Your app decides between touch and mouse: the chart doesn't detect it. Here, a coarse pointer
// (a finger) means touch. The buttons override it, to try touch mode with a mouse
const touchScreen = matchMedia("(pointer: coarse)");
type TMode = "auto" | "touch" | "mouse";
let mode: TMode = "auto";
const isTouch = () => mode === "touch" || (mode === "auto" && touchScreen.matches);

let current: { financeChartPanel: ISciFinanceChartPanelApi; drawing: DrawingToolsController; tools: DrawingToolbar };
let crosshair = false;

// 2. Create the chart in the mode. Some settings are read only when a chart is created, so to
// change mode, create the chart again (save it with toJSON() first to keep its studies and drawings)
async function createChart(touch: boolean) {
current?.tools.destroy();
current?.drawing.detach();
current?.financeChartPanel.cleanup();

const financeChartPanel = await SciFinanceChartPanel.create(document.getElementById("chart"), {
exchangeName: CoinbaseExchange.name, // name="Coinbase" matches the built-in CoinbaseExchange
symbol: "BTC-USD",
interval: CandleInterval.ONE_HOUR,
period: "1W",
studies: [new OHLCStudy(), new VolumeStudy(), new RSIStudy()],
// Touch: each pane's control bar sits in a drawer at its right edge (there is no hover to
// show it), and the dividers between panes get a wider grip
paneControls: { touch },
});
const chart = financeChartPanel.financeApi;

// Touch: a long press selects a drawing, and placement uses a cursor you drag to aim and tap to
// commit. A finished drawing isn't selected, so its editor doesn't cover it. When a tool
// finishes, resumeGestures turns panning back on, unless the crosshair has it
const drawing = DrawingToolsController.attach(chart, {
touch,
selectFinishedDrawing: !touch,
resumeGestures: () => setPanAndZoom(chart, !(touch && crosshair)),
});
const tools = DrawingToolbar.create(document.getElementById("tools"), drawing, { orientation: "vertical" });
current = { financeChartPanel, drawing, tools };

// The crosshair and legend follow the mouse, so they start on with a mouse, and off on touch
setCrosshair(!touch);
}

// 3. The crosshair. With a mouse, hovering moves it and dragging pans. On touch, one finger does
// both, so while the crosshair is on, a drag moves it and panning is off
function setCrosshair(on: boolean) {
crosshair = on;
const chart = current.financeChartPanel.financeApi;
// FinChartLegendModifier draws the crosshair and the legend
chart.finChartModifiers
.asArray()
.find((m): m is FinChartLegendModifier => m instanceof FinChartLegendModifier)
?.setCursorEnabled(on);
if (isTouch()) setPanAndZoom(chart, !on);
renderToolbar();
}

// Pan, X-axis drag and double-tap zoom-to-fit, on every pane. Pinch-to-zoom works on touch screens
// without any setup
function setPanAndZoom(chart: SciFinanceChart, enabled: boolean) {
chart.chartPanes.forEach(({ surface }) =>
surface.chartModifiers.asArray().forEach((m) => {
if (m instanceof ZoomPanModifier || m instanceof XAxisDragModifier || m instanceof FinChartZoomExtentsModifier) {
m.isEnabled = enabled;
}
})
);
}

function renderToolbar() {
const toolbar = document.getElementById("toolbar");
Object.assign(toolbar.style, toolbarStyle);
const modes: Array<[TMode, string]> = [
["auto", `Auto (${touchScreen.matches ? "touch" : "mouse"})`],
["touch", "Touch"],
["mouse", "Mouse"],
];
toolbar.replaceChildren(
...modes.map(([value, label]) =>
button(label, value === mode, () => {
mode = value;
createChart(isTouch());
})
),
button(`Crosshair ${crosshair ? "on" : "off"}`, crosshair, () => setCrosshair(!crosshair))
);
}

function button(text: string, selected: boolean, onClick: () => void) {
const b = document.createElement("button");
b.textContent = text;
Object.assign(b.style, buttonStyle(selected));
b.onclick = onClick;
return b;
}

// In Auto, follow the screen when it changes, e.g. a tablet's keyboard and trackpad attached
touchScreen.addEventListener("change", () => mode === "auto" && createChart(isTouch()));

// 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",
gap: "4px",
padding: "8px 12px",
background: "var(--st-ui-background, #2b2839)",
borderBottom: "1px solid var(--st-ui-divider, rgba(255,255,255,0.1))",
};

const buttonStyle = (selected: boolean): Partial<CSSStyleDeclaration> => ({
padding: "4px 12px",
border: "1px solid var(--st-ui-divider, rgba(255,255,255,0.15))",
borderRadius: "4px",
background: selected ? "var(--st-ui-accent, #5c5cff)" : "var(--st-ui-surface, #363345)",
color: "var(--st-ui-text, #e8e8ee)",
font: "13px inherit",
cursor: "pointer",
});

createChart(isTouch());

React Example: Touch and Mouse Modes​

The same in React. A useSyncExternalStore hook follows the media query, and the chart’s key changes with the mode, so React creates it again.

The code for this example is found below.

import "scichart-scitrader/scitrader.css";
import * as React from "react";
import { XAxisDragModifier, ZoomPanModifier } from "scichart";
import {
CandleInterval,
CoinbaseExchange,
defaultExchangeProvider,
FinChartLegendModifier,
FinChartZoomExtentsModifier,
idbPriceStore,
ISciFinanceChartPanelApi,
ISciFinanceChartPanelConfig,
OHLCStudy,
RSIStudy,
SciFinanceChart,
VolumeStudy,
} from "scichart-scitrader";
import { DrawingToolbar, SciFinanceChartView, useDrawingTools } from "scichart-scitrader-react";

// Register a data source, once, at app startup
defaultExchangeProvider.registerExchange(CoinbaseExchange, idbPriceStore);

// 1. Your app decides between touch and mouse: the chart doesn't detect it. This hook follows a
// media query: a coarse pointer (a finger) means touch. Add a width test for a phone layout
const touchScreen = matchMedia("(pointer: coarse)");
function useTouchScreen(): boolean {
return React.useSyncExternalStore(
(onChange) => {
touchScreen.addEventListener("change", onChange);
return () => touchScreen.removeEventListener("change", onChange);
},
() => touchScreen.matches,
() => false // server render: assume a mouse
);
}

type TMode = "auto" | "touch" | "mouse";

export function TouchModeChart() {
const isTouchScreen = useTouchScreen();
const [mode, setMode] = React.useState<TMode>("auto"); // the buttons override it, to try touch with a mouse
const touch = mode === "touch" || (mode === "auto" && isTouchScreen);

const modes: Array<[TMode, string]> = [
["auto", `Auto (${isTouchScreen ? "touch" : "mouse"})`],
["touch", "Touch"],
["mouse", "Mouse"],
];

return (
<div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
{/* 2. Some settings are read only when a chart is created, so a new `key` creates the
chart again in the new mode */}
<ModeChart key={touch ? "touch" : "mouse"} touch={touch}>
{modes.map(([value, label]) => (
<button key={value} style={buttonStyle(value === mode)} onClick={() => setMode(value)}>
{label}
</button>
))}
</ModeChart>
</div>
);
}

function ModeChart({ touch, children }: { touch: boolean; children: React.ReactNode }) {
const [api, setApi] = React.useState<ISciFinanceChartPanelApi>();
// The crosshair and legend follow the mouse, so they start on with a mouse, and off on touch
const [crosshair, setCrosshair] = React.useState(!touch);
const crosshairRef = React.useRef(crosshair);
crosshairRef.current = crosshair;

// Touch: a long press selects a drawing, and placement uses a cursor you drag to aim and tap to
// commit. A finished drawing isn't selected, so its editor doesn't cover it. When a tool
// finishes, resumeGestures turns panning back on, unless the crosshair has it.
// useDrawingTools reads its options once, which is why the chart is created again on a change
const drawingOptions = React.useMemo(
() => ({
touch,
selectFinishedDrawing: !touch,
resumeGestures: () => api && setPanAndZoom(api.financeApi, !(touch && crosshairRef.current)),
}),
[touch, api]
);
const { controller } = useDrawingTools(api, drawingOptions);

// 3. The crosshair. With a mouse, hovering moves it and dragging pans. On touch, one finger does
// both, so while the crosshair is on, a drag moves it and panning is off
React.useEffect(() => {
if (!api) return;
const chart = api.financeApi;
// FinChartLegendModifier draws the crosshair and the legend
chart.finChartModifiers
.asArray()
.find((m): m is FinChartLegendModifier => m instanceof FinChartLegendModifier)
?.setCursorEnabled(crosshair);
if (touch) setPanAndZoom(chart, !crosshair);
}, [api, crosshair, touch]);

// 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.ONE_HOUR,
period: "1W",
studies: [new OHLCStudy(), new VolumeStudy(), new RSIStudy()],
}),
[]
);

return (
<>
<div style={toolbarStyle}>
{children}
<button style={buttonStyle(crosshair)} onClick={() => setCrosshair(!crosshair)}>
Crosshair {crosshair ? "on" : "off"}
</button>
</div>
<div style={{ display: "flex", flex: 1, minHeight: 0 }}>
<DrawingToolbar controller={controller} orientation="vertical" />
<div style={{ flex: 1, minWidth: 0 }}>
{/* Touch: each pane's control bar sits in a drawer at its right edge (there is no
hover to show it), and the dividers between panes get a wider grip */}
<SciFinanceChartView config={config} paneControls={{ touch }} onInit={setApi} />
</div>
</div>
</>
);
}

// Pan, X-axis drag and double-tap zoom-to-fit, on every pane. Pinch-to-zoom works on touch screens
// without any setup
function setPanAndZoom(chart: SciFinanceChart, enabled: boolean) {
chart.chartPanes.forEach(({ surface }) =>
surface.chartModifiers.asArray().forEach((m) => {
if (m instanceof ZoomPanModifier || m instanceof XAxisDragModifier || m instanceof FinChartZoomExtentsModifier) {
m.isEnabled = enabled;
}
})
);
}

// 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",
gap: 4,
padding: "8px 12px",
background: "var(--st-ui-background, #2b2839)",
borderBottom: "1px solid var(--st-ui-divider, rgba(255,255,255,0.1))",
};

const buttonStyle = (selected: boolean): React.CSSProperties => ({
padding: "4px 12px",
border: "1px solid var(--st-ui-divider, rgba(255,255,255,0.15))",
borderRadius: 4,
background: selected ? "var(--st-ui-accent, #5c5cff)" : "var(--st-ui-surface, #363345)",
color: "var(--st-ui-text, #e8e8ee)",
font: "13px inherit",
cursor: "pointer",
});

Touch Mode API​

The IPaneControlOptions interface, used by paneControls, with its TSDoc
/**
* The per-pane controls: the control bar and the dividers between panes.
*/
interface IPaneControlOptions {
/** Show each pane's control bar. Default: `true`. */
visible?: boolean;
/**
* Touch layout, for phones and tablets: the control bar sits in a drawer at each pane's right
* edge (tap or swipe its handle) instead of appearing on hover, and the dividers have a wider
* hit area with grip dots. The host decides when to use it, e.g. from the viewport size.
* Default: `false`.
*/
touch?: boolean;
/** Buttons to leave out of the control bar, e.g. `{ axisSync: false }`. Default: all shown. */
buttons?: IPaneControlBarButtons;
}
The IDrawingToolsControllerOptions interface, with its TSDoc
interface IDrawingToolsControllerOptions {
/**
* Where the full annotation editor (every option of the selected drawing) is mounted when
* the user opens it from the floating quick-edit toolbar. Omit to offer the quick-edit
* toolbar only.
*/
editorContainer?: HTMLElement;
/**
* Called when the full editor opens, from the quick-edit toolbar or {@link DrawingToolsController.openEditor},
* e.g. to show the panel that holds it.
*/
onOpenFullEditor?: (annotation: IAnnotation) => void;
/** Class added to the full editor's root element in {@link editorContainer}. */
editorRootClassName?: string;
/** Class added to the full editor's panel. */
editorPanelClassName?: string;
/**
* For a touch layout: a long press on a drawing selects it, as a click does with a mouse, and
* placement uses a movable cursor: drag to aim, tap to commit each point. The cursor applies
* to a mouse too, so turn this on for touch devices only, e.g. from `(pointer: coarse)`.
* `true` uses the default selection timings; pass options to change them.
*/
touch?: boolean | IAnnotationTouchSelectionModifierOptions;
/**
* Whether a drawing stays selected, showing the quick-edit toolbar, once it is placed.
* Default `true`. Turn it off where an open editor would cover the chart, as on a phone.
*/
selectFinishedDrawing?: boolean;
/**
* Restores chart gestures when a tool finishes or is stopped. The controller suspends pan,
* x-axis drag and zoom-to-extents while a tool is active, so a drag draws instead of panning.
* By default it turns them back on. Pass this if your app sometimes keeps them off, such as a
* touch "cursor mode" where a drag moves the crosshair instead.
*/
resumeGestures?: () => void;
}
The crosshair members of FinChartLegendModifier, with their TSDoc
class FinChartLegendModifier {
/**
* Master on/off for the cursor readout. When `enabled` is `false`, the
* per-pane legend and the crosshair are hidden completely and pointer moves
* are ignored — the chart shows no cursor/legend. When `true`, the legend
* renders again (latest value at rest, hovered value on pointer-move) and
* the crosshair tracks the pointer.
*
* This modifier — not SciChart's built-in `CursorModifier` (never
* instantiated here) — owns both the crosshair and the legend, so the
* cursor-toggle button routes through this method.
*/
setCursorEnabled(enabled: boolean): void;
/** Whether the cursor readout (crosshair + legend) is currently shown. See {@link setCursorEnabled}. */
get cursorEnabled(): boolean;
}