> ## Documentation Index
> Fetch the complete documentation index at: https://react-native-livechart.brandtnewlabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Manual Y-range scaling

> Drive TradingView-style price-axis scaling from your own gesture.

<Warning>
  **Experimental.** `yRangeScale` is available on `LiveChart` (line and candle modes) and
  `LiveChartSeries`. The library owns the scale math, while your app owns the gesture and reset
  behavior.
</Warning>

`LiveChart` normally fits its Y-axis to the visible data. Pass a `SharedValue<number>` through
`yRangeScale` to multiply that fitted range around its midpoint: `1` keeps auto-fit, values above
`1` show more vertical range, and values between `0` and `1` zoom in.

<Note>
  **Live example:**
  [`app/demo/y-range-scale.tsx`](https://github.com/brandtnewlabs/react-native-livechart/blob/main/app/demo/y-range-scale.tsx)
  includes line/candle/multi-series toggles, preset buttons, a draggable price gutter, and
  double-tap reset. Turn on **Zero floor** and choose **8×** to see how a
  `nonNegative` chart stops its zoom-out at `0`.
</Note>

<Frame caption="Drag the price gutter to stretch or compress the fitted Y-range">
  <video autoPlay loop muted playsInline controls poster="/media/y-range-scale.jpg" src="https://mintcdn.com/brandtnewlabs/urFPpaRiPl9ITH8x/media/y-range-scale.mp4?fit=max&auto=format&n=urFPpaRiPl9ITH8x&q=85&s=8e42b8a70e8dbb0536608011d4899352" data-path="media/y-range-scale.mp4" />
</Frame>

```tsx theme={null}
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import {
  cancelAnimation,
  useSharedValue,
  withTiming,
} from "react-native-reanimated";

const yRangeScale = useSharedValue(1);
const startScale = useSharedValue(1);

const priceScalePan = Gesture.Pan()
  .onStart(() => {
    cancelAnimation(yRangeScale);
    startScale.set(yRangeScale.get());
  })
  .onUpdate((event) => {
    const next = startScale.get() * Math.exp(event.translationY / 160);
    yRangeScale.set(Math.max(0.25, Math.min(4, next)));
  });

const reset = () => {
  yRangeScale.set(withTiming(1, { duration: 240 }));
};

<LiveChart data={data} value={value} yRangeScale={yRangeScale} />;

// The same SharedValue scales every visible series around their shared fit.
<LiveChartSeries series={series} yRangeScale={yRangeScale} />;
```

Mount `priceScalePan` on your own axis-gutter view with `GestureDetector`. Keeping that gesture
outside the chart lets you choose its hit area and compose it with the rest of your screen.

## Behavior

* The multiplier is applied after the normal margin and minimum-range fit, so auto-fit continues
  tracking the visible data while scaled.
* On `LiveChartSeries`, the multiplier applies to the shared range after hidden series are excluded.
* While the SharedValue changes, the displayed range tracks it without frame-lag. Once it stops,
  ordinary range changes use `smoothing` or the optional `rangeAnimation` overrides.
* Reset with an animation such as `withTiming(1)` for the same smooth result whether the current
  scale is above or below `1`.
* On a `nonNegative` chart, zoom-out stops when the lower bound reaches `0`. The upper bound does
  not keep growing after the floor is reached, so the values remain centered instead of being
  pinned to the bottom of the plot.
* `maxValue` is applied after scaling, so that hard ceiling still wins.
* Values must be positive and finite. Invalid values safely behave as `1`; clamp gesture output to
  a product-appropriate range as shown above.

`yRangeScale` changes the value range only. It composes with [`timeScroll` and `zoom`](/guides/time-scroll),
which control the visible time window.

## Animating automatic range changes

By default, both charts snap outward bounds immediately and ease inward bounds with `smoothing`.
Opt into outward easing with `rangeAnimation={{ animateExpansion: true }}`. This also applies
when a reference line enters the fit, without changing recorded values or moving the time window.

For separate speeds, use the exported `RangeAnimationConfig`:

```tsx theme={null}
import { useState } from "react";
import { LiveChartSeries, type RangeAnimationConfig } from "react-native-livechart";
import { useReducedMotion } from "react-native-reanimated";

const RETURN_MS = 300;
// Close 99.9% of a stationary gap over this window, rather than setting a duration.
const RETURN_SPEED = 1 - 0.001 ** (1000 / 60 / RETURN_MS);

// Inside your component; series is your existing SharedValue<SeriesConfig[]>.
const [held, setHeld] = useState(false);
const reduceMotion = useReducedMotion();
const rangeAnimation: RangeAnimationConfig = {
  animateExpansion: held && !reduceMotion,
  expansionSmoothing: 0.08,
  contractionSmoothing: RETURN_SPEED,
};

<LiveChartSeries
  series={series}
  referenceLines={[{ id: "consensus", value: 80, excludeFromRange: !held }]}
  rangeAnimation={rangeAnimation}
  smoothing={reduceMotion ? 1 : 0.08}
  scrub={{ panGestureDelay: 250 }}
  onGestureStart={() => setHeld(true)}
  onGestureEnd={() => setHeld(false)}
/>;
```

The return speed applies directly to contraction on the UI thread, so release needs no timer
or temporary change to global `smoothing`. It also applies to other inward fits while configured.
Omitted direction speeds inherit `smoothing`; `expansionSmoothing` only takes effect while
`animateExpansion` is true. Each bound chooses its own direction, so one can expand while the
other contracts. Finite speeds clamp to 0..1; non-finite speeds inherit `smoothing`.

On initial fitting or a data change that moves the fitted range wholly above or below
the displayed range, contraction is limited to the animated expansion speed until
the ranges overlap. This keeps the Y-axis bounds ordered even with very slow or frozen
expansion; normal independent speeds resume once the ranges overlap.

These speeds are frame-rate-independent exponential easing fractions, **not fixed durations**.
The 300 ms calculation assumes a stationary target; live data can continue moving the fit.
`smoothing={1}` takes precedence over the overrides, as do `snapKey` changes, static mode,
and active manual `yRangeScale` dragging. For reduced motion, use `smoothing={1}` and configure
axis-label fades with `metrics={{ grid: { fadeInSpeed: 1, fadeOutSpeed: 1 } }}` as needed.

Axis-label alpha transitions remain controlled by `metrics.grid.fadeInSpeed` and
`fadeOutSpeed`. The chart, reference lines, and overlays using `ChartOverlayContext.scale`
share the interpolated bounds throughout expansion and contraction. `transitions` continues
to control reveal/mode changes separately.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.