Skip to content

View and price scale ​

Move the view from code ​

ts
chart.scrollToRealtime();               // newest bar in view
chart.fitContent();                     // all bars in view, panes autoscaled
chart.setVisibleTimeRange(from, to);    // unix seconds
chart.resetView();                      // default zoom, newest bar, autoscale (also Shift+End)
chart.getVisibleRange();                // { from, to } or null
chart.scrollToTime(time);               // centre a time, keep the zoom

These animate by default. Pass false as the last argument to jump instead:

ts
chart.scrollToRealtime(false);
chart.setVisibleTimeRange(from, to, false);

Animations are skipped when animateViewChanges is off or the user prefers reduced motion, and any user input cancels them.

fitContent() and setVisibleTimeRange() return false when the range holds more bars than fit at minBarSpacing; the view then shows the newest part of it. See limits, and grouping to fit any number of bars.

getVisibleRange() returns what setVisibleTimeRange(from, to) was given when both times are bar times. A view call made while a setSymbol load is pending is applied again once the new bars arrive, so switching the interval and then setting a range works in one go.

Limit where the view can go ​

By default the user can pan until bars cover only half the plot (minBarCoverage: 0.5) at either end. A share of the plot width behaves the same at every zoom, where a bar count would leave a sliver of data when zoomed out and a wide block when zoomed in. Tighten that with options:

ts
createChart(el, {
  fixLeftEdge: true,     // no further left than the first bar
  fixRightEdge: true,    // no further right than the newest bar plus rightOffset
  maxRightOffset: 20,    // at most 20 empty bars right of the newest bar
  minBarCoverage: 0.8,   // bars keep covering 80% of the plot when panning to either end
});
  • The limits apply everywhere the view moves: dragging, inertia (which stops at an edge), wheel and pinch zoom, the keyboard, and scrollToRealtime, fitContent, setVisibleTimeRange and resetView, whose animations ease to the nearest allowed view.
  • Zooming keeps the bar under the pointer in place unless that would cross a fixed edge; then the edge stays put.
  • With fixLeftEdge and a right limit (fixRightEdge or maxRightOffset), zooming out stops once every bar fits. When there are too few bars to fill the plot even at maxBarSpacing, the first bar stays at the left edge and zoom keeps its full range.
  • When maxRightOffset is smaller than rightOffset, fitContent, setVisibleTimeRange and a range kept across an interval switch use maxRightOffset as the margin after the newest bar, so getVisibleRange() still returns the requested range.
  • applyOptions applies new limits to the current view straight away. minBarSpacing, maxBarSpacing and maxRightOffset must be finite and non-negative, minBarCoverage between 0 and 1, and rightOffset finite; an invalid value throws and leaves the options unchanged.

Viewport bounds ​

To keep the user inside a time window or a price band, for a replay that hides the future, a backtest viewer or an embedded lesson, set viewport bounds:

ts
chart.setViewportBounds({
  time: { from: start, to: replayTime },        // unix seconds; either end may be left out
  price: { low: 90, high: 140 },                 // optional; also `pane` and `scale`
});
chart.viewportBounds;            // a copy of what was set, or null
chart.setViewportBounds(null);   // remove them
  • The plot shows only bars from the first one at or after from to the last one at or before to. Panning slides along those edges without changing the zoom, and zooming out stops once the whole window fills the plot (it never goes below minBarSpacing). The bounds take precedence over fixLeftEdge, fixRightEdge, maxRightOffset and minBarCoverage, and follow the bars when history is prepended.
  • Every path obeys: dragging, inertia, wheel, pinch, the keyboard, and scrollToRealtime, fitContent, setVisibleTimeRange and resetView. When one of those four asks for more than the bounds allow, the chart shows what it can and emits viewport:clamp { axis: 'time', requested, applied } with both ranges in unix seconds.
  • A price bound keeps one price scale (the main pane's right one unless pane and scale say otherwise) inside low..high through axis drags, zoom, pan, autoscale, setPriceRange and a locked price-to-bar ratio. Setting it turns that scale's autoscale off; clearing the bounds turns it back on. A setPriceRange the band narrows emits viewport:clamp { axis: 'price', pane, scale, requested, applied } with both ranges in prices.
  • setSymbol with another symbol or interval clears the bounds, unless keepOnContextChange: true was set with them.
  • Bounds that are not finite, have from after to or low not below high, or name a missing pane throw at the call.

Viewport bounds are opt-in: create the chart with the viewportBounds() plugin (or builtins()). The UI shell adds it for you. Without it, setViewportBounds throws not-registered and viewportBounds is null.

Group bars when zoomed out ​

Grouping comes with the grouping() plugin, which builtins() includes. Without it the grouping option is kept but bars draw singly, zooming stops at minBarSpacing, chart.grouping is null, and development builds warn once (grouping-missing).

ts
import { createChart, grouping } from '@chart-on/core';

const chart = createChart(el, { plugins: [grouping()], grouping: { mode: 'pixel' } });
chart.fitContent(); // true for any number of bars
chart.on('grouping:change', (g) => console.log(g ? `${g.bars} bars per bucket` : 'single bars'));

With grouping, zooming out can go past minBarSpacing, down to half the spacing that fits every bar. Once single bars get narrower than about two device pixels, the chart draws buckets of 2 ** level bars instead, choosing the level that keeps each bucket at least two device pixels wide, so the work per frame follows the plot width, not the number of bars:

  • A candle bucket is one bar: the first open, highest high, lowest low and last close (and summed volume for volume candles). Per-bar colours are not used for buckets. Line-like candle styles draw one point per bucket.
  • A histogram column shows the bucket's groupBy: 'sum' (the default, for volume), 'max' or 'last'; its colour is the last point's.
  • Line series and indicator line plots draw each bucket's first, lowest, highest and last value at its centre, so spikes stay visible; autoscale reads them in time that grows with the logarithm of the point count. Indicator histogram plots show each bucket's last value, and fills run between the plots' bucket values. Custom series show one item per bucket: what their group hook makes of the bucket's items, else its last item (see custom series).
  • Buckets sit on fixed bar indices (bucket j holds bars j * 2 ** level to (j + 1) * 2 ** level - 1), so panning never changes what a bucket holds. They are built the first time they are needed and kept up to date as bars arrive or the newest bar ticks.
  • Autoscale reads candle buckets in time that grows with the logarithm of the bar count.
  • The crosshair sits on the bucket's centre and its time label shows the bucket's first and last bar time. The legend shows the bucket's values, and valuesAt adds group: { time, endTime, count }.
  • chart.grouping is { level, bars } while the view groups bars and null otherwise; grouping:change fires when that changes. Indicators compute, and drawings and annotations work, on the real bars and times.

grouping: null (the default) leaves everything as without it.

Group by calendar period ​

{ mode: 'calendar' } groups by calendar periods instead of powers of two, so a zoomed-out chart of minute bars shows the hourly, daily or weekly bars the same data has at that interval:

ts
chart.applyOptions({ grouping: { mode: 'calendar' } });                  // 1m, 5m, 15m, 1h, 4h, 1d, 1w, 1M
chart.applyOptions({ grouping: { mode: 'calendar', steps: ['1h', '1d', '1w'] } });
chart.grouping; // { level: 0, bars: 390, step: '1d' }
  • The chart picks the finest step longer than the bar interval whose buckets average at least about two device pixels, counting the bars the data really has per period (sessions and short days included). Past the coarsest step it keeps that one.
  • Periods follow the chart's timezone and session: days and longer are trading days (an overnight session's evening bars belong to the next day), weeks start on Monday, and shorter steps follow the local clock, so buckets stay on local boundaries across daylight-saving changes. Closed time has no bars, so it has no buckets either.
  • A bucket holds the bars of one period, so buckets hold different numbers of bars. Bodies and columns are drawn as wide as the narrowest bucket in view, so they never overlap; the crosshair, legend and valuesAt give the period's own first and last bar time and bar count.
  • Everything else works as in pixel mode. Buckets are worked out once per step and extended as bars arrive, so a live bar updates only the last bucket.

Resizing and new bars ​

A resize keeps the bar spacing by default, so a wider chart shows more bars. With lockVisibleRangeOnResize: true the bar spacing scales with the plot width instead and getVisibleRange() stays the same (within minBarSpacing and maxBarSpacing). This applies to any change of the plot width, including a price axis appearing or hiding, and to a chart that was hidden (zero width) and shown again.

followNewBars decides what happens when bars are appended:

ValueBehaviour
'whenAtEnd' (default)While the newest bar is on screen the view scrolls with new bars; otherwise it stays on the bars the user is looking at
'always'Like 'whenAtEnd', and a new bar also scrolls back from older bars to the newest one, with the configured rightOffset, stopping any inertia or eased zoom
'never'The view never moves; new bars fill the empty space on the right and then go off screen

When data changes ​

What the view does when new bars arrive:

ChangeZoom (bar spacing)Position
First load of a symbolbarSpacing optionNewest bars, with rightOffset
setSymbol to another symbolKept (keepZoomOnSymbolChange, default true); off, back to barSpacing and every pane autoscaledNewest bars, with rightOffset. A view wider than the first load pages in older history until it is filled
setSymbol to another interval of the same symbolKeptNewest bars; with keepRangeOnIntervalChange, the same start and end times (the newest bar when the view reached it) after paging in the history they need
setData on the main seriesKeptSame distance from the newest bar, so a longer or shorter series shifts the times in view. With { keepVisibleRange: true }, the same start and end times when the new bars cover them
Older history pageKeptUnchanged: the bars in view stay put
New bar (update, live data)KeptSee followNewBars above

A view change made while a setSymbol load is pending (setVisibleTimeRange, fitContent, scrollToRealtime, resetView) wins over the rows above. Otherwise new data never resets the price scale: a scale you dragged keeps its range until you call resetView or turn autoscale back on, also on a symbol with a different price level.

Bar spacing and logical indices ​

Every bar has a logical index: 0 is the oldest loaded bar, and the count goes on past the newest bar into the empty space on the right (and below 0 to the left). Bar i is centred on index i, and fractional indices fall between bars.

ts
chart.barSpacing;                        // px per bar
chart.setBarSpacing(12);                 // zoom, keeping the right edge in place
chart.setBarSpacing(12, x);              // zoom around chart-root x, like the mouse wheel
chart.getVisibleIndexRange();            // { from, to } fractional indices at the plot edges, or null
chart.setVisibleIndexRange(100.5, 180);  // show exactly that span (animates; pass false to jump)

Like setVisibleTimeRange, setVisibleIndexRange turns the main pane's autoscale back on, except on a scale with a locked price-to-bar ratio.

These go through the same limits as panning and zooming with the mouse: minBarSpacing and maxBarSpacing, and the rule that keeps some bars on screen. Near the ends of the data the view you get can therefore differ from the one you asked for; read it back with getVisibleIndexRange(). They throw a RangeError for non-finite values, a spacing of 0 or less, or from >= to.

Time after the last bar ​

The space right of the newest bar has times too: drawings placed there, timeToX for a future time, the crosshair time label, and the time-axis labels and vertical grid lines, which continue into it. They follow a projected bar calendar rather than a fixed step, so a point placed "next Monday" from Friday's last daily bar lands on the next bar slot, not two weekend slots later:

BarsNext bar
Intraday, with a sessionThe interval while inside the session, then the next session open (holidays, one-day overrides and overnight sessions included)
Daily, with a sessionThe next day the session trades
Daily, without oneThe next day on a weekday the recent bars fall on, so stock data skips weekends on its own
Intraday without a session, weekly and longerThe fixed bar interval

The projection follows the chart's timezone, and it moves as soon as a bar arrives or the session changes. Far ahead (past 20,000 projected bars) it continues with the fixed interval. Before the oldest bar, times are still extrapolated by the bar interval.

Series points can sit on the projected bars too, for a forecast or a known future schedule. A point after the newest bar is drawn when its time is a projected bar time; chart.timeline.projected(n) gives the next n:

ts
const last = bars[bars.length - 1];
const forecast = chart.addLine({ title: 'Forecast', lineStyle: 'dashed' });
forecast.setData([
  { time: last.time, value: last.close },
  ...chart.timeline.projected(20).map((time, k) => ({ time, value: last.close + drift * (k + 1) })),
]);

These points count in autoscale and show in the legend like any other while in view. When a bar arrives where the projection had none (an unexpected session), they move with the projection to keep their times. A point between projected bars (a closed day, say) is not drawn, as for a point between real bars.

Position HTML over the chart ​

To put your own HTML on the chart (a callout, a badge, an order ticket), convert data positions to pixels. All of these use chart-root css px: offsets from the top-left corner of chart.root, the same frame pointAt and the pointer events use.

MethodReturns
coordinateFor(time, price?, pane = 0){ x, y } for a bar centre, with y: null without a price. pointAt snaps x to the nearest bar, so this is its inverse at bar centres
timeToX(time) / xToTime(x)x of a time (extrapolated past the data) / the time of the nearest bar
indexToX(index) / xToIndex(x)x of a fractional logical index, and back
priceToY(price, pane = 0, scale?) / yToPrice(y, pane?, scale?)y of a price on a pane's scale, and back. yToPrice without a pane uses the pane under y
paneBounds(){ pane, top, height }[]; a pane hidden because another is maximized has height 0
plotRect{ x, y, width, height } of the plot area, without the axes
priceAxisSpace('left' | 'right')Space all of that side's price axis columns take (one priceAxisWidth column per scale, or the measured width in auto mode)

priceToY and yToPrice use the pane's primary scale (the one pointAt reads) unless you pass a scale id such as 'left'. They work in prices in every mode: in percent or indexed mode pass the price, not the percentage. Looking up a scale id that does not exist returns null and does not create the scale.

The conversions return null instead of throwing before the chart has a size, without data (for the x conversions), for a pane that does not exist or is hidden, for an unknown scale id, for non-finite input, and for a price at or below zero on a log scale (it has no position there). A position outside the visible area still converts; check it against plotRect and paneBounds() if you only want to show what is in view.

Positions change with every pan, zoom, resize, pane change and autoscale step. The conversions read the scales as the last frame drew them, so right after a call that moves the view (such as setBarSpacing or setPriceScaleMode) a price position is only valid after the next frame. view:change fires after that frame, at most once per frame, whenever anything a position depends on moved, so re-place your elements there rather than right after the call. Its payload is { barSpacing, fromIndex, toIndex }, the fractional logical indices at the plot edges. Here is a callout that follows a bar's high:

ts
const callout = document.createElement('div');
callout.className = 'callout';
callout.textContent = 'Earnings';
Object.assign(callout.style, { position: 'absolute', pointerEvents: 'none', transform: 'translate(-50%, -100%)' });
chart.root.appendChild(callout);

const bar = { time: 1_700_000_000, high: 182.4 };

function place(): void {
  const at = chart.coordinateFor(bar.time, bar.high);
  const y = at?.y ?? null;
  const plot = chart.plotRect;
  const pane = chart.paneBounds()[0];
  const visible =
    at !== null && y !== null && pane !== undefined &&
    at.x >= plot.x && at.x <= plot.x + plot.width &&
    y >= pane.top && y <= pane.top + pane.height;
  callout.style.display = visible ? '' : 'none';
  if (visible) {
    callout.style.left = `${at.x}px`;
    callout.style.top = `${y - 6}px`;
  }
}

const off = chart.on('view:change', place);
place();
// later: off(); callout.remove();

chart.root is positioned, so absolutely positioned children line up with chart-root px. Keep pointer-events: none on decorations so the chart still gets the pointer; leave it on for elements people click, such as a ticket's buttons.

Price scale mode ​

Each price scale has one of four modes:

ModeShows
normalLinear prices
logLogarithmic prices, for long histories or big moves
percentChange relative to a base value, by default the first visible one
indexedPrices indexed to 100 at the base value
ts
chart.setPriceScaleMode('log');              // main pane, right scale
chart.setPriceScaleMode('percent', 1);       // second pane
chart.setPriceScaleMode('indexed', 0, 'left'); // left scale of the main pane
chart.getPriceScaleMode();                   // 'normal' | 'log' | 'percent' | 'indexed'

Log mode is applied on the GPU, so switching modes does not re-upload data.

Percent base ​

Percent and indexed modes measure from the first visible value unless a scale's percentBase says otherwise:

percentBaseMeasures from
'firstVisible' (default)The close of the first visible bar, which moves as you pan
'sessionOpen'The open of the trading day that holds the newest visible bar
'previousClose'The close of the bar before that trading day
{ time }The close of the first bar at or after time, marked with a small triangle on the time axis
ts
chart.setPriceScaleOptions({ mode: 'percent', percentBase: 'previousClose' });
chart.setPriceScaleOptions({ percentBase: { time: Date.UTC(2024, 0, 2) / 1000 } }); // performance since 2 January

Trading days follow the time axis day boundaries, so the chart's timezone and session schedule apply: an overnight session's evening bars belong to the next day, and Monday's previous close is Friday's last bar. On daily and longer bars every bar is its own session. Series without an open (lines, histograms) use their value at the base bar.

Every series on the scale measures from the same bar, and so do compared symbols linked to it, so their lines stay comparable. The setting is saved in layouts and returned by getPriceScaleOptions.

A log scale needs data above zero. Autoscale fits only the positive values, so a line that crosses zero or a histogram based at 0 keeps a sensible range. Values at or below zero are drawn clipped at the bottom of the pane: histogram columns run off the bottom edge. A histogram's base stays in view only when it is above zero. When nothing visible on the scale is above zero, the scale keeps its last range and the scale:change event reports noPositiveData: true (see Stay in sync); switching to log mode in that state shows prices 1 to 10.

Several price scales ​

A pane can hold more than one price scale, so series in different units share it. Every series and indicator takes a priceScale id (default 'right'):

ts
chart.addCandles({ title: 'Price' });                            // right scale
chart.addLine({ title: 'Open interest', priceScale: 'left' });   // left scale
const flows = chart.addLine({ title: 'Flows', priceScale: 'flows' }); // overlay, no axis
chart.setPriceScaleOptions({ side: 'right' }, 0, 'flows');      // now a second right axis
chart.indicators.add(rsiDef, {}, { priceScale: 'left' });

An indicator definition can name its scale too; see Indicators.

  • 'right' is the pane's default scale (its axis is on the right, or on the left when the chart's direction is right to left) and 'left' gets a left axis. Any other id is created on first use as an overlay with no axis, until setPriceScaleOptions gives it a side.
  • A scale's axis shows while a visible series is on it, and hides when everything on it is hidden. The right axis stays when nothing else on the pane has one.
  • Each side gets as many axis columns as the busiest pane needs, each priceAxisWidth wide.
  • With priceAxisWidth: 'auto' (the default), each side's columns fit the widest label they show: tick labels, the scale's top and bottom prices (which bound the crosshair label), and last-value labels with the countdown. The width is the label inset (8 px), the text and priceAxis.padding, kept between priceAxis.minWidth and priceAxis.maxWidth. A side grows at once but shrinks only when it would lose more than one font size, so prices that gain or lose a digit do not make the plot jump. A width change takes effect in the same frame; after fitContent, setVisibleTimeRange or setVisibleIndexRange the chart keeps that range in view until the user pans or zooms, otherwise it keeps the bar spacing. Pass a number, such as priceAxisWidth: 76, for a fixed width. Read the laid-out space with priceAxisSpace(side) to align your own elements.
  • Last-value labels on one axis move apart instead of covering each other. The crosshair shows a price label on every axis of its pane.
  • Drag an axis to zoom that scale; double-click it to autoscale again.
  • Series with the overlay option keep their own private scale, as before.
  • Drawings, pointer events and the crosshair:move event read prices on the pane's primary scale (pane.primary): the right scale while it shows, else the first scale with an axis. Drawings store prices, so when the primary scale changes (for example, the only right series is hidden and a left one takes over) they are drawn against the new scale and move with it. chart.formatPrice(pane, price) uses its format. Price and trade lines sit on their series' scale and label its axis.
  • setPriceScaleOptions({ side: 'none' }) on the right scale hides its axis even when nothing else on the pane has one.

setPriceScaleOptions(options, pane = 0, scale = 'right') sets side ('left', 'right' or 'none'), mode, invert (higher prices lower down), autoScale, marginTop, marginBottom, fixedRange, autoscaleRule, lockPriceToBarRatio and priceToBarRatio (see below). getPriceScaleOptions(pane, scale) reads them back.

ts
chart.setPriceScaleOptions({ invert: true }); // flip the main pane's right scale

Axis lines, labels and the time axis ​

priceAxis and timeAxis style the axes; applyOptions merges a partial object into each.

ts
chart.applyOptions({
  priceAxis: { ticks: true, fontSize: 11, labelMode: 'content', labelPointer: true },
  timeAxis: { ticks: true, border: false, showSeconds: true },
});
chart.applyOptions({ timeAxis: { visible: false } }); // a sparkline without a time axis
priceAxis fieldDefaultWhat it does
ticksfalseA 4 px mark at each tick label, on the plot edge of its column
bordertrueThe line between the plot and the price axes
fontSizethemeFont size (px) of the price axis text and its filled labels; unset follows theme.typography.axis
labelMode'fill'Filled labels (last value, crosshair, selected drawing) span the column ('fill') or fit their text ('content')
labelPointerfalseFilled labels get a point on their plot side, its tip at the exact price
labelPaddingY4Space (px) above and below a filled label's text; labels on one axis keep this much further apart

padding, minWidth and maxWidth size auto-width axes, above. In auto mode the column also makes room for the label point and measures in fontSize.

timeAxis fieldDefaultWhat it does
visibletrueHidden, the time axis takes no space: the plot reaches the bottom edge, and its tick labels, the crosshair's bottom time label and drawing time labels go with it. A crosshair time label at the top of the plot (crosshair.timeLabelPosition: 'top') stays
height28Height in px
ticksfalseA 4 px mark under each time label
bordertrueThe line between the plot and the time axis
showTimetrueThe built-in crosshair and drawing time labels show the time of day for intraday bars; off, the date only
showSecondsfalseBuilt-in times of day show seconds: tick labels, the crosshair and drawing labels. Useful for bars shorter than a minute
fontSizethemeFont size (px) of the time axis text and its labels

formatTime and formatTimeTick replace the built-in labels, so showTime and showSeconds do not apply to them. The chart settings schema (chartSettingsSchema) has an Axes group with these switches.

Autoscale ​

Autoscale fits the price range to the visible data and eases toward it (smoothScale). Dragging the price axis turns it off for that pane; double-clicking the axis turns it back on.

ts
chart.setAutoScale(false);   // main pane
chart.isAutoScale(1);        // second pane
chart.setAutoScale(true, 0, 'left');

Per-series autoscale ​

By default every visible series fits on its scale. The autoscale option on candle, line, histogram and custom series changes that:

ts
// Shown, but never stretches the price range (it may run off the pane).
chart.addLine({ autoscale: false }).setData(longAverage);

// Adjust the range the series adds: here 5 above and below its values.
chart.addLine({
  autoscale: (range, { mode, from, to }) => range && { low: range.low - 5, high: range.high + 5 },
});

series.applyOptions({ autoscale: true }); // fit its values again

The function gets { low, high } for the series' visible values (null when none are visible) and returns the range to fit, or null to add nothing this frame. The second argument holds the series, the visible logical indices from and to, and the scale mode. Ranges are prices in every mode. On a log scale, values at or below zero are left out. A non-finite result adds nothing. If the function throws, the chart reports it once through series:error (hook 'autoscale') and fits the series' values meanwhile. Primitives attached to a series still add their own ranges. If every series on a scale is left out, the scale keeps its last range.

Autoscale rule ​

autoscaleRule on a price scale adjusts the range fitted to its data before the margins are added:

ts
chart.setPriceScaleOptions({ autoscaleRule: { centerOn: 0 } }, 1); // a histogram around zero
chart.setPriceScaleOptions({ autoscaleRule: { include: [100], minSpan: 2 } });
chart.setPriceScaleOptions({ autoscaleRule: null }); // fit the data as it is
FieldEffect
includePrices the range always reaches, e.g. a target level
minSpanSmallest price span to fit, so flat data does not zoom into noise. The range widens around its middle
centerOnA price kept in the middle, so positive and negative values compare honestly

They apply in that order (include, then minSpan, then centerOn), and only while there is visible data to fit. Values are prices in every mode, like fixedRange, which wins over the rule. On a log scale, include and centerOn skip values at or below zero, centerOn centres on a ratio (10 below 40 means 160 above it), and a minSpan that would reach zero widens upwards instead. setPriceScaleOptions throws a RangeError for a rule that is not finite, a minSpan that is not positive or an unknown field. Layouts save and restore the rule. A pane indicator can declare one in its definition (see Fixed pane range).

Set and read a price range ​

setPriceRange shows exactly a price range and turns autoscale off; getPriceRange reads what a scale shows. Both use prices in every mode (log, percent and indexed included), with from the lower price and to the higher, also on an inverted scale (where from is at the top). Use them to restore a saved view or to give two charts the same price range.

ts
chart.setPriceRange({ from: 60000, to: 68000 });         // main pane, eased with `true` as 4th argument
chart.setPriceRange({ from: 0, to: 50 }, 1, 'right', true);
const { from, to } = chart.getPriceRange()!;              // null for a pane or scale that does not exist
other.setPriceRange(chart.getPriceRange()!);

A log scale needs prices above zero; equal, non-finite or (on a log scale) non-positive prices throw a RangeError. Double-click the axis or call setAutoScale(true) to fit the data again.

Fixed range ​

fixedRange gives a scale a range that autoscale always shows, whatever the data, with no margins. It suits oscillators bounded to 0–100:

ts
chart.setPriceScaleOptions({ fixedRange: { min: 0, max: 100 } }, 1);
chart.setPriceScaleOptions({ fixedRange: null }, 1); // fit the data again

Like setPriceRange, min and max are prices in every mode, percent and indexed included. Setting a fixed range at or below zero on a scale that is already log throws a RangeError. Switching a scale with such a range (say 0–100) to log does not throw: while in log mode the scale ignores the fixed range and fits the data above zero as usual, and the fixed range applies again once the scale leaves log mode.

Zooming the axis by hand still turns autoscale off, and turning it back on returns to the fixed range.

A pane indicator can declare a fixed range in its definition (see Fixed pane range); the built-in RSI pane opens on 0–100.

On a percent or indexed scale, compared symbols keep sharing the axis while it shows a fixed, manual or locked range: their lines are placed on the same percent range the axis shows.

Lock the price-to-bar ratio ​

With lockPriceToBarRatio, zooming the time axis scales prices by the same factor, so the angle of a trend line or a drawing stays the same as you zoom. Locking turns autoscale off, and turning autoscale on (setAutoScale(true), double-clicking the axis, resetView or setPriceScaleMode) unlocks it. fitContent, setVisibleTimeRange and setVisibleIndexRange keep the lock, so the angles stay put through them too. Changing the mode with setPriceScaleOptions({ mode }) keeps the lock and takes the ratio again in the new mode's units, and so does dragging the price axis or calling setPriceRange while locked. The UI shell's chart settings have it for the main pane, as Keep line angles when zooming (the priceScale.lockPriceToBarRatio setting).

priceToBarRatio is the locked ratio, in price-scale coordinate units per bar width (log10 units in log mode). Save it and pass it back to restore the same view later. It reads null while unlocked, or while locked before the chart is laid out; passing null (or leaving it out) with lockPriceToBarRatio: true locks at the current ratio, so options read back can always be passed back:

ts
chart.setPriceScaleOptions({ lockPriceToBarRatio: true });
const saved = chart.getPriceScaleOptions()!.priceToBarRatio;
// later
chart.setPriceScaleOptions({ lockPriceToBarRatio: true, priceToBarRatio: saved });

Stay in sync ​

scale:change fires whenever a scale's mode, inversion, autoscale, fixed range or ratio lock changes, from code or from the user. Use it to keep your own buttons in sync.

ts
chart.on('scale:change', ({ pane, scale, mode, autoScale, invert }) => {
  if (pane === 0 && scale === 'right') renderScaleButtons(mode, autoScale);
});

It also fires when noPositiveData changes: true while a log scale has visible data but none of it above zero, for example to offer switching back to normal.

The UI's price scale menu (scaleMode(), in the default toolbar) and the inline scaleModeInline() controls are wired this way: linear, log or percent (an indexed scale shows as percent) and auto.