---
title: Pipsend Charts for Flutter
description: Documentation for pipsend_charts, the Flutter trading charting package with candlestick, bar, line and Heikin Ashi charts, 15 technical indicators, interactive drawing tools, replay mode and infinite history.
---

# Pipsend Charts for Flutter

`pipsend_charts` is a charting package for Flutter trading apps. It renders interactive financial charts (candlesticks, OHLC bars, line and Heikin Ashi) with technical indicators, draggable trading lines, price zones, Fibonacci tools, drawing tools, a TradingView-style crosshair, replay mode and lazy loading of historical data.

This page documents package version 1.7.1.

## Overview and requirements

### Highlights

- Candlestick, OHLC bar, line and Heikin Ashi chart types
- Pinch-to-zoom, pan, vertical zoom/pan and drag-to-scale on the price axis
- 15 technical indicators (moving averages, oscillators, volatility, volume, sessions)
- Trading lines (entry, stop loss, take profit, alert, support, resistance) and price zones (supply, demand, support, resistance)
- Fibonacci retracement, extension and fan, trend lines, and eight more drawing tools, all draggable
- Managers with an event system and JSON serialization (`toJson` / `fromJson`, `serializeAll` / `restoreAll`)
- Configurable grid, adaptive labels, rounded candles, light and dark styling
- Infinite history (lazy loading) through `onXOffsetChanged`
- Replay mode with a draggable playhead and tick-by-tick animation
- Tap-to-select drawings, persistent crosshair and a drawing placement mode

### Requirements

From the package `pubspec.yaml`:

| Requirement | Value |
|---|---|
| Dart SDK | `>=2.15.0 <4.0.0` |
| Flutter | `>=2.0.0` |
| Dependencies | `intl` (`>=0.17.0 <0.21.0`) |

The package is built for Flutter mobile apps (iOS and Android). Its README also lists Web, macOS, Windows and Linux as supported platforms.

### Links

- Package: [pub.dev/packages/pipsend_charts](https://pub.dev/packages/pipsend_charts)
- Source and issues: [github.com/ArcaTechCo/PipsendChartsFl](https://github.com/ArcaTechCo/PipsendChartsFl)
- Example app: the `example` folder of the repository

## Installation

Add the dependency to your `pubspec.yaml`:

```yaml
dependencies:
  pipsend_charts: ^1.7.1
```

Then run:

```bash
flutter pub get
```

Import the package. Everything documented on this page is exported from this single entry point:

```dart
import 'package:pipsend_charts/pipsend_charts.dart';
```

## Quick start

### Basic chart

```dart
import 'package:pipsend_charts/pipsend_charts.dart';

InteractiveChart(
  candles: candleData,
  style: ChartStyle(
    priceGainColor: Colors.green,
    priceLossColor: Colors.red,
  ),
)
```

`InteractiveChart` fills the space its parent gives it, so place it inside an `Expanded`, a `SizedBox` or a `Scaffold` body.

### With indicators

```dart
InteractiveChart(
  candles: candleData,
  indicators: [
    RSIIndicator(period: 14),
    MACDIndicator(fastPeriod: 12, slowPeriod: 26, signalPeriod: 9),
    BollingerBandsIndicator(period: 20, stdDev: 2.0),
  ],
)
```

### With trading lines

```dart
final lineManager = TradingLineManager();

// Add a stop loss
lineManager.addLine(TradingLine(
  id: 'sl_1',
  price: 150.0,
  type: TradingLineType.stopLoss,
  options: TradingLineOptions(
    title: 'Stop Loss',
    draggable: true,
    onPriceChanged: (newPrice) {
      lineManager.updateLinePrice('sl_1', newPrice);
    },
  ),
));

InteractiveChart(
  candles: candleData,
  overlays: lineManager.lines,
)
```

### With price zones

```dart
final zoneManager = PriceZoneManager();

// Add a demand zone
zoneManager.addZone(PriceZone(
  id: 'demand_1',
  minPrice: 140.0,
  maxPrice: 145.0,
  type: PriceZoneType.demand,
  options: PriceZoneOptions(
    label: 'Demand Zone',
    draggable: true,
    onRangeChanged: (newMin, newMax, newStartTime, newEndTime) {
      zoneManager.updateZoneRange('demand_1', newMin, newMax,
          startTime: newStartTime, endTime: newEndTime);
    },
  ),
));

InteractiveChart(
  candles: candleData,
  overlays: zoneManager.zones,
)
```

## Chart widget and parameters

`InteractiveChart` is a `StatefulWidget`. Only `candles` is required.

### Data and view

| Parameter | Type | Default | Description |
|---|---|---|---|
| `candles` | `List<CandleData>` | required | Full series, oldest first. Must contain at least 3 candles. |
| `initialVisibleCandleCount` | `int` | `90` | Candles shown when the chart first opens. Must be 3 or more. |
| `style` | `ChartStyle` | `ChartStyle()` | Visual configuration. See [Theming](#theming). |
| `indicators` | `List<Indicator>` | `[]` | Technical indicators. See [Indicators](#indicators). |
| `overlays` | `List<ChartOverlay>` | `[]` | Trading lines, zones and drawing tools. |
| `futureCandles` | `int` | `0` | Empty candle-widths shown to the right of the last candle, so users can scroll into and draw in the future. A value of 50 gives roughly half a screen at default zoom. |
| `freeCamera` | `bool` | `false` | When `true`, scrolling is not constrained to the data range. |
| `initialVerticalZoom` | `double` | `1.0` | `1.0` fits candles to the height, above `1.0` adds padding above and below, below `1.0` crops. |
| `enableVerticalPan` | `bool` | `false` | Enables vertical zoom and pan. See [Vertical zoom and pan](#vertical-zoom-and-pan). |
| `controller` | `InteractiveChartController?` | `null` | Programmatic control. See [Controller and callbacks](#controller-and-callbacks). |
| `showWatermark` | `bool` | `true` | Shows the Pipsend Charts logo in the bottom-left corner. |

### Labels and info panel

| Parameter | Type | Description |
|---|---|---|
| `timeLabel` | `TimeLabelGetter?` | `String Function(int timestamp, int visibleDataCount)`. Formats the labels on the time axis. |
| `crosshairTimeLabel` | `CrosshairTimeLabelGetter?` | `String Function(int timestamp)`. Formats the date/time readout under the crosshair. Defaults to a locale-aware `"EEE d MMM yy · HH:mm"`. |
| `priceLabel` | `PriceLabelGetter?` | `String Function(double price)`. Formats the price labels. Defaults to two decimals. |
| `overlayInfo` | `OverlayInfoGetter?` | `Map<String, String> Function(CandleData candle)`. Customizes the OHLC panel shown when the user touches the chart. |

Example of a custom info panel:

```dart
overlayInfo: (candle) => {
  "Date": "Customized date string goes here",
  "Open": candle.open?.toStringAsFixed(2) ?? "-",
  "Close": candle.close?.toStringAsFixed(2) ?? "-",
},
```

### Interaction and callbacks

| Parameter | Type | Default | Description |
|---|---|---|---|
| `enableInteraction` | `bool` | `true` | Tap shows the candle details panel. Set to `false` for read-only or embedded charts. |
| `onTap` | `ValueChanged<TapDetails>?` | `null` | Fired when the user taps a candle. |
| `onCandleResize` | `ValueChanged<double>?` | `null` | Fired on zoom with the candle width at the current zoom level. |
| `onXOffsetChanged` | `ValueChanged<XAxisOffsetDetails>?` | `null` | Fired on pan or zoom. Used for [infinite history](#infinite-history-lazy-loading). |
| `persistentCrosshair` | `bool` | `false` | Keeps the crosshair visible after the finger is lifted. Requires `enableInteraction`. |
| `onCrosshairActiveChanged` | `ValueChanged<bool>?` | `null` | Fired when a long-press toggles the crosshair. |
| `onOverlayTapped` | `ValueChanged<ChartOverlay?>?` | `null` | Fired with the tapped interactive overlay, or `null` for empty space. |
| `selectedOverlayId` | `String?` | `null` | Id of the overlay the host marks as selected. It is drawn with an emphasized border and handles. |
| `onSelectedOverlayRectChanged` | `ValueChanged<Rect?>?` | `null` | Reports the bounding rect of the selected overlay as the chart pans and zooms. Currently emitted for `PriceZone` overlays. |
| `placement` | `DrawingPlacement?` | `null` | Enters drawing placement mode. See [Drawing placement mode](#drawing-placement-mode). |
| `onPlacementComplete` | `ValueChanged<List<PlacementPoint>>?` | `null` | Fired when all placement points are confirmed. |

### Replay

| Parameter | Type | Description |
|---|---|---|
| `playheadIndex` | `int?` | Index of the last visible candle. `null` means no replay. Must satisfy `0 <= playheadIndex < candles.length`. |
| `playheadTickProgress` | `double?` | Sub-candle progress between `0.0` and `1.0`. |
| `playheadStyle` | `PlayheadStyle?` | Built-in playhead appearance. When `null` the playhead is not drawn. |
| `onPlayheadChanged` | `ValueChanged<PlayheadInfo>?` | Fired when the user drags the built-in playhead. |

See [Replay mode](#replay-mode).

## Data model

### CandleData

```dart
CandleData(
  timestamp: DateTime.now().millisecondsSinceEpoch,
  open: 100.0,
  high: 105.0,
  low: 98.0,
  close: 103.0,
  volume: 1000000.0,
)
```

| Field | Type | Notes |
|---|---|---|
| `timestamp` | `int` | Required. Milliseconds since epoch. |
| `open` | `double?` | Required argument, may be `null`. A `null` open or close shows as a gap. |
| `close` | `double?` | Required argument, may be `null`. |
| `volume` | `double?` | Required argument, may be `null`. |
| `high` | `double?` | Optional. If `high` or `low` is `null`, the wick is not drawn. |
| `low` | `double?` | Optional. |
| `trends` | `List<double?>` | Optional extra trend line values for this candle, for example `[ma7, ma30]`. The list is made unmodifiable, so assign a new list instead of mutating it. |

Keep the candles sorted from oldest to newest.

### Helpers on CandleData

- `CandleData.computeMA(List<CandleData> data, [int period = 7])` returns a list of moving average values (the first `period` entries are `null`) that you can assign to `trends`.
- `CandleData.toHeikinAshi(List<CandleData> data)` converts a full series to Heikin Ashi. It must run over the whole series because the Heikin Ashi open is recursive.
- `CandleData.buildPartial(real, progress)` returns an interpolated candle for tick replay.

### Loading candles from an API

```dart
Future<List<CandleData>> loadCandles() async {
  final response = await http.get(Uri.parse('api-url'));
  final data = json.decode(response.body) as List;

  return data.map((item) => CandleData(
    timestamp: item['timestamp'],
    open: item['open'].toDouble(),
    high: item['high'].toDouble(),
    low: item['low'].toDouble(),
    close: item['close'].toDouble(),
    volume: item['volume']?.toDouble() ?? 0.0,
  )).toList();
}
```

### TapDetails

Passed to `onTap`.

| Field | Type | Description |
|---|---|---|
| `candle` | `CandleData?` | The tapped candle, or `null` when tapping the empty future space. |
| `tapPrice` | `double` | Price at the tap position. |
| `candleIndex` | `int` | Index of the tapped candle, or `-1` in future space. |
| `timestamp` | `int?` | Candle timestamp, or an extrapolated timestamp in future space. |
| `isFutureSpace` | `bool` (getter) | `true` when `candle` is `null`. |

## Real-time updates

`InteractiveChart` renders the list you pass in. To stream live data, update your list inside `setState` and give the chart a new list (or a new `CandleData` for the forming candle). `CandleData` fields are `final`, so replace the last element instead of editing it. The chart compares candles item by item, so rebuilding with identical instances does not trigger a repaint.

```dart
class RealtimeChart extends StatefulWidget {
  @override
  _RealtimeChartState createState() => _RealtimeChartState();
}

class _RealtimeChartState extends State<RealtimeChart> {
  List<CandleData> _data = [];
  final _controller = InteractiveChartController();
  bool _autoScroll = true;

  void _addNewCandle(CandleData newCandle) {
    setState(() {
      _data.add(newCandle);
    });

    // Auto-scroll to latest if enabled
    if (_autoScroll) {
      WidgetsBinding.instance.addPostFrameCallback((_) {
        _controller.jumpToLatest();
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return InteractiveChart(
      candles: _data,
      controller: _controller,
    );
  }
}
```

For a streaming source such as a WebSocket, see [Integration guide](#integration-guide).

## Chart types

Select how the main price series is drawn with `ChartStyle.chartType`:

| `ChartType` | Description |
|---|---|
| `candlestick` | Japanese candlesticks. This is the default. |
| `bar` | OHLC bars: a vertical high/low line with a left tick at the open and a right tick at the close. |
| `line` | A single path through the closes. The price axis is bounded by the closes instead of the highs and lows. |
| `heikinAshi` | Heikin Ashi candles, derived internally and cached. |

```dart
InteractiveChart(
  candles: candleData,
  style: ChartStyle(
    chartType: ChartType.line,
    lineColor: Colors.blue,   // falls back to priceGainColor
    lineWidth: 1.6,           // default
  ),
)
```

With `ChartType.heikinAshi`, indicators, the price axis and the crosshair OHLC panel all report Heikin Ashi values. Overlays such as trading lines, zones and Fibonacci tools keep their real prices, so they sit on the Heikin Ashi axis rather than on the original candles.

### Rounded candles

```dart
InteractiveChart(
  candles: candleData,
  style: ChartStyle(
    candleBorderRadius: 4.0,  // 0.0 = square, 8.0 = very rounded
    priceGainColor: Colors.green,
    priceLossColor: Colors.red,
  ),
)
```

`candleBorderRadius` only affects the candle body, not the wicks. Suggested values: `0.0` square (default), `2.0` slightly rounded, `4.0` moderately rounded, `8.0` very rounded.

### Volume

Volume bars are shown by default. Control them with `ChartStyle.showVolume`, `volumeColor` and `volumeHeightFactor` (fraction of the chart height, default `0.2`).

## Indicators

Pass indicators with the `indicators` parameter. Every indicator accepts an optional `id`, `panel`, `style` and `visible` (default `true`). Indicators are cached; call `clearCache()` on an indicator to force a recalculation.

```dart
InteractiveChart(
  candles: data,
  indicators: [
    RSIIndicator(period: 14),
    MACDIndicator(
      fastPeriod: 12,
      slowPeriod: 26,
      signalPeriod: 9,
    ),
    BollingerBandsIndicator(
      period: 20,
      stdDev: 2.0,
    ),
    SMAIndicator(period: 20),
    EMAIndicator(period: 12),
  ],
)
```

### Available indicators

| Indicator | Constructor parameters (defaults) | Style class | Default placement |
|---|---|---|---|
| `SMAIndicator` | `period` (20) | `SMAStyle` | Overlay |
| `EMAIndicator` | `period` (12) | `EMAStyle` | Overlay |
| `WMAIndicator` | `period` (20) | `WMAStyle` | Overlay |
| `BollingerBandsIndicator` | `period` (20), `stdDev` (2.0) | `BollingerBandsStyle` | Overlay |
| `FractalsIndicator` | none | `FractalsStyle` | Overlay |
| `VolumeProfileIndicator` | `bins` (24), `showPOC` (true), `showValueArea` (true) | `VolumeProfileStyle` | Overlay (fixed) |
| `TradingSessionsIndicator` | none | `TradingSessionsStyle` | Overlay (fixed) |
| `RSIIndicator` | `period` (14), `overbought` (70), `oversold` (30) | `RSIStyle` | Separate panel, height 0.2 |
| `MACDIndicator` | `fastPeriod` (12), `slowPeriod` (26), `signalPeriod` (9) | `MACDStyle` | Separate panel, height 0.25 |
| `StochasticIndicator` | `kPeriod` (14), `dPeriod` (3), `overbought` (80), `oversold` (20) | `StochasticStyle` | Separate panel, height 0.2 |
| `ATRIndicator` | `period` (14) | `ATRStyle` | Separate panel, height 0.15 |
| `ADXIndicator` | `period` (14) | `ADXStyle` | Separate panel, height 0.2 |
| `CCIIndicator` | `period` (20), `overbought` (100), `oversold` (-100) | `CCIStyle` | Separate panel, height 0.2 |
| `WilliamsRIndicator` | `period` (14), `overbought` (-20), `oversold` (-80) | `WilliamsRStyle` | Separate panel, height 0.2 |
| `OBVIndicator` | none | `OBVStyle` | Separate panel, height 0.2 |

`VolumeProfileIndicator` and `TradingSessionsIndicator` do not accept a `panel` argument; they are always drawn over the main chart.

### Panels

The `panel` parameter takes an `IndicatorPanel`:

```dart
IndicatorPanel.overlay({String? priceScaleId, bool showGrid = false})
IndicatorPanel.separate({
  double height = 0.2,
  String? priceScaleId,
  bool showGrid = true,
  bool showPriceScale = true,
})
```

### Style examples

```dart
RSIIndicator(
  period: 14,                          // Default: 14
  style: RSIStyle(
    lineColor: Colors.purple,
    overboughtLevel: 70,
    oversoldLevel: 30,
  ),
)
```

```dart
MACDIndicator(
  fastPeriod: 12,
  slowPeriod: 26,
  signalPeriod: 9,
  style: MACDStyle(
    macdLineColor: Colors.blue,
    signalLineColor: Colors.orange,
    histogramPositiveColor: Colors.green,
    histogramNegativeColor: Colors.red,
  ),
)
```

```dart
BollingerBandsIndicator(
  period: 20,
  stdDev: 2.0,
  style: BollingerBandsStyle(
    upperBandColor: Colors.red,
    middleBandColor: Colors.blue,
    lowerBandColor: Colors.green,
  ),
)
```

### Style fields

| Style class | Fields (defaults) |
|---|---|
| `SMAStyle` | `lineColor` (blue), `lineWidth` (2.0) |
| `EMAStyle` | `lineColor` (orange), `lineWidth` (2.0) |
| `WMAStyle` | `lineColor` (purple), `lineWidth` (2.0) |
| `ATRStyle` | `lineColor` (orange), `lineWidth` (2.0) |
| `RSIStyle` | `lineColor`, `lineWidth` (2.0), `overboughtColor`, `oversoldColor`, `referenceLinesColor`, `overboughtLevel` (70), `oversoldLevel` (30) |
| `MACDStyle` | `macdLineColor`, `macdLineWidth`, `signalLineColor`, `signalLineWidth`, `histogramPositiveColor`, `histogramNegativeColor`, `zeroLineColor` |
| `BollingerBandsStyle` | `upperBandColor`, `upperBandWidth`, `middleBandColor`, `middleBandWidth`, `lowerBandColor`, `lowerBandWidth`, `fillBands` (true), `fillColor` |
| `StochasticStyle` | `kLineColor` (blue), `dLineColor` (red), `lineWidth`, `overboughtColor`, `oversoldColor` |
| `ADXStyle` | `adxLineColor` (blue), `plusDIColor` (green), `minusDIColor` (red), `lineWidth` |
| `CCIStyle` | `lineColor` (purple), `lineWidth`, `overboughtColor`, `oversoldColor`, `zeroLineColor` |
| `WilliamsRStyle` | `lineColor` (teal), `lineWidth`, `overboughtColor`, `oversoldColor` |
| `OBVStyle` | `lineColor` (cyan), `lineWidth`, `zeroLineColor` |
| `FractalsStyle` | `upColor`, `downColor`, `markerSize` (6.0), `markerGap` (6.0) |
| `VolumeProfileStyle` | `barColor`, `pocBarColor`, `pocLineColor`, `pocLineWidth`, `valueAreaColor`, `borderColor`, `labelColor`, `showLabels`, `widthFactor` (0.15) |
| `TradingSessionsStyle` | `showTokyo`, `showLondon`, `showNewYork`, `showLabels`, `tokyoColor`, `londonColor`, `newYorkColor`, `overlapColor`, `labelTextColor` |

## Drawing tools and gestures

All drawing tools are `ChartOverlay` objects. Pass them in the `overlays` list, either directly or through a manager. Every overlay has an `id` (optional in most constructors, but provide a unique one for drag and drop and serialization), a `visible` flag, and an `options` object that holds `draggable` and the callbacks.

The chart is fully controlled by your app. When the user drags an overlay, the chart calls the `onMoved`-style callback with the new coordinates. You then store the new values (directly or through a manager) and rebuild the chart.

### Trading lines

Horizontal price levels.

```dart
TradingLine(
  id: 'sl_1',
  price: 150.0,
  type: TradingLineType.stopLoss,
  options: TradingLineOptions(title: 'Stop Loss', draggable: true),
)
```

`TradingLine({String? id, required double price, required TradingLineType type, TradingLineStyle? style, TradingLineOptions? options, bool visible = true, int? startTime, int? endTime})`

**Types** (`TradingLineType`):

- `entry` - entry point
- `stopLoss` - stop loss level
- `takeProfit` - take profit target
- `alert` - alert level (dashed by default)
- `support` - support level
- `resistance` - resistance level
- `custom` - custom line

**TradingLineOptions** (defaults): `showLabel` (true), `showPrice` (true), `title`, `customText`, `customTextColor`, `customTextFontSize`, `customTextBackgroundColor`, `labelPosition` (`LabelPosition.left`; also `right`, `center`, `custom`), `customLabelOffset`, `priceFormatter`, `draggable` (true), `deletable` (true), `onTap`, `onPriceChanged`, `onPriceDragging`, `onDelete`.

**TradingLineStyle**: `color` (required), `lineWidth` (1.0), `dashPattern`, `labelColor`, `labelFontSize` (10), `labelHorizontalPadding` (2), `labelVerticalPadding` (1), `labelBackgroundColor`, `priceColor`, `priceFontSize` (10), `priceBackgroundColor`. Factories: `TradingLineStyle.fromType(type)`, `.solid(color)`, `.dashed(color)`, `.dotted(color)`.

Passing `startTime` and `endTime` anchors the line to a time range. Without them the line extends across the whole chart. A line that has `draggable: true` or an `onTap` callback is interactive.

**Manager** (`TradingLineManager`):

```dart
// Add line
String? id = lineManager.addLine(line);

// Update price
lineManager.updateLinePrice(id, newPrice);

// Remove line
lineManager.removeLine(id);

// Query
List<TradingLine> lines = lineManager.getLinesByType(TradingLineType.stopLoss);

// Clear all
lineManager.clear();

// Listen to events
lineManager.addListener((event) {
  print('Line ${event.type}: ${event.line?.id}');
});
```

Other members include `addLines`, `removeLines`, `getLineById`, `updateLine`, `setLineVisibility`, `toggleLineVisibility`, `visibleLines`, `stopLossLines`, `takeProfitLines`, `entryLines`, `alertLines`, `supportLines`, `resistanceLines`, `getLinesInRange`, `findClosestLine`, `clearByType`, `count`, `isEmpty`, `hasLine`, and sorting helpers (`sortByPrice`, `sortByType`).

### Price zones

Rectangular zones between two prices.

`PriceZone({String? id, required double minPrice, required double maxPrice, required PriceZoneType type, PriceZoneStyle? style, PriceZoneOptions? options, bool visible = true, int? startTime, int? endTime})`

**Types** (`PriceZoneType`): `support`, `resistance`, `demand`, `supply`, `custom`.

**PriceZoneOptions**: `label`, `showLabel` (true), `draggable` (false), `resizable` (false), `onRangeChanged(minPrice, maxPrice, startTime, endTime)`, `onDelete`.

**PriceZoneStyle**: `fillColor` (required), `borderColor`, `borderWidth` (1.0), `showBorder` (true), `labelStyle`.

Zones support full 2D movement and corner-handle resizing. `PriceZone.boundingRect(params)` exposes the zone bounds.

**Manager** (`PriceZoneManager`):

```dart
// Add zone
String id = zoneManager.addZone(zone);

// Update range
zoneManager.updateZoneRange(id, newMin, newMax);

// Remove zone
zoneManager.removeZone(id);

// Query
List<PriceZone> zones = zoneManager.getZonesByType(PriceZoneType.demand);

// Clear all
zoneManager.clear();
```

Other members include `addZones`, `getZoneById`, `updateZone`, `setZoneVisibility`, `toggleZoneVisibility`, `supportZones`, `resistanceZones`, `demandZones`, `supplyZones`, `getZonesAtPrice`, `getZonesInRange`, `findClosestZone`, `clearByType`.

### Fibonacci retracement

```dart
final fibManager = FibonacciManager();

fibManager.addFibonacci(FibonacciRetracement(
  id: 'fib_1',
  highPrice: 200.0,
  lowPrice: 150.0,
  options: FibonacciOptions(
    showLabels: true,
    showPercentages: true,
    draggable: true,
    onMoved: (newHigh, newLow, newStartTime, newEndTime) {
      fibManager.updateFibonacciRange('fib_1', newHigh, newLow,
          startTime: newStartTime, endTime: newEndTime);
    },
  ),
));

InteractiveChart(
  candles: candleData,
  overlays: fibManager.fibonaccis,
)
```

`FibonacciRetracement({String? id, required double highPrice, required double lowPrice, FibonacciStyle? style, FibonacciOptions? options, bool visible = true, int? startTime, int? endTime})`

**FibonacciOptions** (defaults): `draggable` (true), `showLabels` (true), `showPercentages` (true), `showPrices` (true), `extendLines` (false), `onMoved`, `onDelete`.

**FibonacciStyle**: `level0Color`, `level236Color`, `level382Color`, `level50Color`, `level618Color`, `level786Color`, `level100Color`, `lineWidth`, `labelStyle`.

Retracement supports full 2D dragging with top-left and bottom-right corner handles.

### Fibonacci extension

Three-point projection (points A, B and C, each independently draggable). Levels: 0%, 38.2%, 61.8%, 100%, 127.2%, 161.8%, 200%, 261.8%.

`FibonacciExtension({String? id, required double pointAPrice, required int pointATime, required double pointBPrice, required int pointBTime, required double pointCPrice, required int pointCTime, FibonacciStyle? style, FibonacciExtensionOptions? options, bool visible = true})`

`FibonacciExtensionOptions({draggable = true, showLabels = true, onMoved, onDelete})`, where `onMoved` has the signature `(int, double, int, double, int, double)`.

### Fibonacci fan

Radial Fibonacci lines from a pivot point (levels 38.2%, 50%, 61.8%).

`FibonacciFan({String? id, required double startPrice, required int startTime, required double endPrice, required int endTime, FibonacciStyle? style, FibonacciFanOptions? options, bool visible = true})`

`FibonacciFanOptions({draggable = true, showLabels = true, onMoved, onDelete})`, where `onMoved` has the signature `(int, double, int, double)`.

### Trend lines

```dart
final trendManager = TrendLineManager();

trendManager.addTrendLine(TrendLine(
  id: 'trend_1',
  startTime: startTimestamp,
  startPrice: 100.0,
  endTime: endTimestamp,
  endPrice: 150.0,
  options: TrendLineOptions(
    draggable: true,
    showAngle: true,
    extendRight: false,
    onMoved: (newStartTime, newStartPrice, newEndTime, newEndPrice) {
      trendManager.updateTrendLinePoints('trend_1',
        startTime: newStartTime,
        startPrice: newStartPrice,
        endTime: newEndTime,
        endPrice: newEndPrice,
      );
    },
  ),
));

InteractiveChart(
  candles: candleData,
  overlays: trendManager.trendLines,
)
```

**TrendLineOptions** (defaults): `draggable` (true), `extendRight` (false), `extendLeft` (false), `showAngle` (false), `onMoved`, `onDelete`.

**TrendLineStyle**: `color` (blue), `lineWidth` (2.0), `dashPattern` (empty), `labelStyle`.

### More drawing tools

These tools are standalone overlays. Group them with `ChartDrawingManager` if you want to serialize them. All options classes include `draggable` (default `true`); the callback argument lists are shown below.

| Tool | Constructor (required arguments) | `onMoved` / change callback |
|---|---|---|
| `PositionTool` | `entryPrice`, `stopLossPrice`, `takeProfitPrice` | `PositionToolOptions.onPositionChanged(double entry, double stopLoss, double takeProfit)` |
| `RulerTool` | `startTime`, `startPrice`, `endTime`, `endPrice` | `(int startTime, double startPrice, int endTime, double endPrice)` |
| `VerticalLine` | `timestamp` | `(int newTimestamp)` |
| `ArrowTool` | `startTime`, `startPrice`, `endTime`, `endPrice` | `(int startTime, double startPrice, int endTime, double endPrice)` |
| `CircleTool` | `centerTime`, `centerPrice`, `radiusTime`, `radiusPrice` | `(int centerTime, double centerPrice, int radiusTime, double radiusPrice)` |
| `TextTool` | `timestamp`, `price`, `text` | `(int timestamp, double price)` plus `onEdit` |
| `BrushTool` | `points` (`List<BrushPoint>`) | `(int timeOffset, double priceOffset)` |
| `GanttTool` | `startTime`, `endTime`, `price` (optional `height` 5.0, `label`) | `(int startTime, int endTime, double price)` |

Each constructor also takes an optional `id`, `style`, `options` and `visible`. Style and options classes follow the naming pattern `<Tool>Style` and `<Tool>Options` (for example `ArrowToolStyle`, `RulerToolOptions`).

Notes on individual tools:

- **PositionTool**: the position is treated as long when `entryPrice > stopLossPrice`, and as short otherwise. `PositionToolOptions` has `showLabels`, `showPriceInLabel`, `showRiskReward`, `showPositionZone` (false) and `draggable`. Entry, stop loss and take profit drag independently.
- **RulerTool**: shows price difference, percentage, number of candles and elapsed time. `RulerToolOptions` has `showPrice`, `priceDecimals` (2), `showPercentage`, `showPips` (false), `pipMultiplier` (10000), `showTime`.
- **VerticalLine**: `VerticalLineOptions` has `label`, `showLabel`, `labelPosition` (`VerticalLineLabelPosition.top` or `bottom`). The default dash pattern is `[5, 3]`.
- **ArrowTool**: `ArrowToolStyle` has `color`, `strokeWidth`, `arrowheadSize` (12.0), `arrowheadAngle` (30.0), `fillArrowhead`.
- **CircleTool**: `CircleToolStyle` has `color`, `strokeWidth`, `filled`, `fillColor`, `fillOpacity` (0.2). Radii are separate for time and price, so it can form an ellipse.
- **TextTool**: double-tap a text tool that has an `onEdit` callback to edit it. `onEdit` is `Future<String?> Function(String currentText)` and returns the new text. Styling includes `textColor`, `fontSize`, `fontWeight`, `fontStyle`, `horizontalAlignment` (`TextHorizontalAlignment`), `verticalAlignment` (`TextVerticalAlignment`), `showBackground`, `backgroundColor`, `padding`, `borderRadius`, `showBorder`, `borderColor`, `borderWidth`.
- **BrushTool**: draws predefined freehand strokes from `BrushPoint(timestamp:, price:)` values with smooth curves. It is a visualization of strokes you supply, not a built-in freehand input mode.
- **GanttTool**: a time-period bar at a price level, with `GanttToolStyle` fields `fillColor`, `fillOpacity`, `borderColor`, `borderWidth`, `borderRadius`, `showLabel`, `labelColor`, `labelFontSize`, `labelBackground`.

Example of a text annotation with editing:

```dart
TextTool(
  id: textId,
  timestamp: _candles[60].timestamp,
  price: _candles[60].close ?? 0,
  text: 'Important note',
  options: TextToolOptions(
    draggable: true,
    onMoved: (newTimestamp, newPrice) {
      setState(() {
        final index = _overlays.indexWhere((o) => o.id == textId);
        if (index >= 0) {
          _overlays[index] = (_overlays[index] as TextTool).copyWith(
            timestamp: newTimestamp,
            price: newPrice,
          );
        }
      });
    },
    onEdit: (currentText) async {
      final controller = TextEditingController(text: currentText);
      return showDialog<String>(
        context: context,
        builder: (context) => AlertDialog(
          title: Text('Edit text'),
          content: TextField(controller: controller, autofocus: true),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(context, null),
              child: Text('Cancel'),
            ),
            TextButton(
              onPressed: () => Navigator.pop(context, controller.text),
              child: Text('Save'),
            ),
          ],
        ),
      );
    },
  ),
)
```

### Placing initial positions with OverlayHelper

`OverlayHelper` calculates sensible initial positions inside the visible price range. Each method returns a `Map<String, double>`.

```dart
// Fibonacci: 40% of the visible range, centered
final position = OverlayHelper.calculateFibonacciPosition(
  minPrice: minVisiblePrice,
  maxPrice: maxVisiblePrice,
  heightPercent: 0.4,  // 40% of visible range
  centerPercent: 0.5,  // Centered vertically
);

fibManager.addFibonacci(FibonacciRetracement(
  id: 'fib_1',
  highPrice: position['highPrice']!,
  lowPrice: position['lowPrice']!,
  options: FibonacciOptions(
    draggable: true,
    onMoved: (high, low, startTime, endTime) {
      fibManager.updateFibonacciRange('fib_1', high, low,
          startTime: startTime, endTime: endTime);
    },
  ),
));
```

Available methods: `calculateTrendLinePosition`, `calculateFibonacciPosition`, `calculatePriceZonePosition` and `calculateTradingLinePrice`. For a price zone, `calculatePriceZonePosition` returns `minPrice` and `maxPrice`. For a trend line it requires `visibleCandleCount`, `minPrice`, `maxPrice`, `latestTimestamp` and `candleInterval`, and returns `startTime`, `endTime`, `startPrice` and `endPrice`.

### Tap-to-select drawings

Use `onOverlayTapped`, `selectedOverlayId` and `onSelectedOverlayRectChanged` to build a TradingView-style selection with a floating toolbar (delete, edit, and so on). `onOverlayTapped` fires with the tapped overlay, or `null` when the user taps empty chart space. `ChartOverlay.selectionBounds(params)` is implemented for price zones, trend lines, Fibonacci tools, arrows, rulers, circles, Gantt bars, text, vertical lines and position tools.

### Drawing placement mode

Placement mode lets users create a drawing by placing its points with a live preview, like TradingView.

```dart
InteractiveChart(
  candles: candleData,
  placement: const DrawingPlacement(
    pointCount: 2,
    preview: PlacementPreviewShape.line,
  ),
  onPlacementComplete: (points) {
    // points[i].price, points[i].timestamp, points[i].localPosition
    // Build the real drawing from these points, then set placement to null.
  },
)
```

`DrawingPlacement({int pointCount = 2, PlacementPreviewShape preview = PlacementPreviewShape.rectangle, Color color = Color(0xFF2962FF)})`. `PlacementPreviewShape` is `none`, `rectangle` or `line`. While placement is active, pan, zoom and overlay editing are suspended and an interactive crosshair with a center dot is shown. Each placed point is returned as a `PlacementPoint` (`price`, `timestamp`, `localPosition`). Your app builds the final drawing and then clears `placement`.

### Gestures

**Chart**

| Gesture | Action |
|---|---|
| Drag | Pan the chart horizontally |
| Pinch | Zoom |
| Tap on a candle | Select it and show the OHLC panel (when `enableInteraction` is `true`) |
| Long-press on empty chart | Toggle the crosshair (when `onCrosshairActiveChanged` is set) |
| Drag on the price scale (right side) with one finger | Scale the Y axis |
| Double-tap the price scale | Reset to auto-fit |
| Vertical drag | Pan vertically (when `enableVerticalPan` is `true`) |

**Overlays**

| Gesture | Action |
|---|---|
| Tap | Select (see `onOverlayTapped`) |
| Drag | Move the overlay |
| Drag a handle | Resize or move a single point |
| Double-tap a `TextTool` | Edit its text (when `onEdit` is set) |

On desktop and web, `Shift + Scroll` zooms vertically and `Alt + Scroll` pans vertically when `enableVerticalPan` is enabled.

### Vertical zoom and pan

```dart
InteractiveChart(
  candles: candleData,
  enableVerticalPan: true,        // Enable vertical controls
  initialVerticalZoom: 1.3,       // 30% padding above/below
  overlays: [
    TradingLine(
      price: 1.2500,
      type: TradingLineType.takeProfit,
      options: TradingLineOptions(title: 'TP'),
    ),
  ],
)
```

Use this to leave room for TP/SL lines and indicators outside the candle range. Dragging on the price scale scales the Y axis even when `enableVerticalPan` is `false`.

### Crosshair

```dart
InteractiveChart(
  candles: candleData,
  persistentCrosshair: true,
  onCrosshairActiveChanged: (active) => setState(() => _crosshairOn = active),
  style: ChartStyle(
    crosshairDashed: true,
    crosshairLabelColor: Colors.blueGrey,
  ),
)
```

- In persistent mode the crosshair is visible from the start at a resting position and moves by the finger's delta, rather than jumping to the touch point.
- Highlighted price (right) and date (bottom) labels follow the crosshair, and the crosshair continues into the empty space past the first and last candle.
- In persistent mode the floating OHLC panel is hidden.
- `ChartStyle.crosshairDashed` draws thin dashed lines on both axes (TradingView-style). `crosshairLabelColor` sets the label background; the text color is chosen automatically for contrast.
- A long-press on empty chart toggles the crosshair. The host reflects the requested state back through `persistentCrosshair`.

### Infinite history (lazy loading)

Load older candles as the user scrolls, similar to TradingView.

```dart
class ChartScreen extends StatefulWidget {
  @override
  _ChartScreenState createState() => _ChartScreenState();
}

class _ChartScreenState extends State<ChartScreen> {
  List<CandleData> _candles = [];
  bool _isLoadingHistory = false;

  @override
  void initState() {
    super.initState();
    _loadInitialData();
  }

  Future<void> _loadInitialData() async {
    // Load initial 200 candles
    final data = await api.fetchCandles(limit: 200);
    setState(() => _candles = data);
  }

  Future<void> _loadMoreHistory() async {
    if (_isLoadingHistory || _candles.isEmpty) return;

    setState(() => _isLoadingHistory = true);

    // Get oldest timestamp
    final oldestTimestamp = _candles.first.timestamp;

    // Load 100 more historical candles
    final olderData = await api.fetchCandles(
      before: oldestTimestamp,
      limit: 100,
    );

    setState(() {
      // Prepend: Add to the beginning
      // Chart automatically maintains visual position
      _candles = [...olderData, ..._candles];
      _isLoadingHistory = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return InteractiveChart(
      candles: _candles,

      // Detect when user scrolls near the beginning
      onXOffsetChanged: (details) {
        // Load more when less than 50 candles before visible area
        if (details.isNearStart(50) && !_isLoadingHistory) {
          // Use Future.microtask to avoid setState during build
          Future.microtask(() => _loadMoreHistory());
        }
      },
    );
  }
}
```

(`api.fetchCandles` stands for your own data source.)

**XAxisOffsetDetails** passed to `onXOffsetChanged`:

| Member | Description |
|---|---|
| `offset` | Current X offset |
| `startCandleIndex`, `endCandleIndex` | Visible range |
| `totalCandles` | Total candles available |
| `visibleCandleCount` | `endCandleIndex - startCandleIndex` |
| `candlesBeforeVisible`, `candlesAfterVisible` | Candles outside the visible range |
| `isAtStart`, `isAtEnd` | Boundary checks |
| `isNearStart([threshold = 50])`, `isNearEnd([threshold = 50])` | Threshold checks |

**Bidirectional loading:**

```dart
onXOffsetChanged: (details) {
  // Load historical data when scrolling left
  if (details.isNearStart(50) && !_isLoadingHistory) {
    Future.microtask(() => _loadMoreHistory());
  }

  // Load recent data when scrolling right
  if (details.isNearEnd(50) && !_isLoadingRecent) {
    Future.microtask(() => _loadMoreRecentData());
  }
}
```

Tips: prepend older data (oldest first) so the chart keeps its visual position, keep a loading flag to avoid duplicate requests, and defer `setState` with `Future.microtask`.

### Replay mode

Render the chart as if frozen at any historical moment. Candles past the playhead are hidden, while indicators keep computing over the full series so their cache stays warm. The playhead can be dragged like a video scrubber, and tick replay smoothly interpolates the in-progress candle.

```dart
InteractiveChart(
  candles: candles,           // full history (load once, large buffer)
  playheadIndex: playhead,    // last visible candle
  playheadTickProgress: tick, // 0.0..1.0 - sub-candle for tick mode
  playheadStyle: const PlayheadStyle(
    lineColor: Colors.blueAccent,
    dimRightSide: true,       // translucent overlay over the "future"
    draggable: true,
  ),
  onPlayheadChanged: (info) {
    // Update your replay state with info.candleIndex / info.candleTimestamp
  },
  controller: replayController, // optional, for seekToIndex/seekToTimestamp
)
```

Key behaviors:

- The chart's effective length becomes `playheadIndex + 1`. Pan and zoom limits, `XAxisOffsetDetails.totalCandles` and `jumpToLatest()` all honor it.
- `CandleData.buildPartial(real, progress)` replaces the playhead candle for smooth tick animation when `playheadTickProgress` is set.
- A single-finger drag on the playhead is consumed before pan and zoom. Pinch still zooms normally.
- `PlayheadInfo` has `candleIndex`, `candleTimestamp` and `interpolatedTimestamp`.

**PlayheadStyle** (defaults): `lineColor` (`0xFF3B82F6`), `lineWidth` (1.5), `dashPattern`, `showLabel` (true), `labelStyle`, `labelBackground`, `labelHorizontalPadding` (6), `labelVerticalPadding` (3), `draggable` (true), `hitRadius` (12), `dimRightSide` (true), `dimColor`, `dimOpacity` (0.25), `showDragHandles` (true), `handleSize` (8).

A complete play/pause/speed/scrub example using a timer:

```dart
InteractiveChart(
  candles: _candles,
  controller: _controller,
  playheadIndex: _playhead,
  playheadTickProgress: _mode == _Mode.tick ? _tickProgress : null,
  playheadStyle: const PlayheadStyle(
    lineColor: Color(0xFF3B82F6),
    lineWidth: 1.5,
    dashPattern: [4, 3],
    dimRightSide: true,
    dimOpacity: 0.22,
  ),
  onPlayheadChanged: (info) {
    setState(() {
      _playhead = info.candleIndex;
      _tickProgress = 0.0;
    });
  },
  indicators: [
    SMAIndicator(period: 20),
    SMAIndicator(period: 50),
  ],
  futureCandles: 20,
  initialVisibleCandleCount: 60,
)
```

On every timer tick, advance `_playhead` (or `_tickProgress` in tick mode) inside `setState`, then call `_controller.seekToIndex(_playhead)` to keep the playhead in view ("follow mode").

## Grid and labels

### Grid

```dart
InteractiveChart(
  candles: data,
  style: ChartStyle(
    gridStyle: GridStyle.full,  // Show both horizontal and vertical
  ),
)
```

Custom grid:

```dart
InteractiveChart(
  candles: data,
  style: ChartStyle(
    gridStyle: GridStyle(
      // Horizontal (price) grid
      showHorizontalGrid: true,
      horizontalLineStyle: GridLineStyle.solid,
      horizontalStrokeWidth: 1.0,
      horizontalGridColor: Colors.grey.withOpacity(0.3),

      // Vertical (time) grid
      showVerticalGrid: true,
      verticalLineStyle: GridLineStyle.dashed,
      verticalStrokeWidth: 0.5,
      verticalGridColor: Colors.grey.withOpacity(0.1),
    ),
  ),
)
```

`GridStyle` defaults: horizontal grid on, vertical grid off, color grey at 30% opacity, stroke width `0.5`, `GridLineStyle.solid`.

**Presets:**

```dart
GridStyle.none          // No grid lines
GridStyle.horizontalOnly // Only horizontal (default)
GridStyle.full          // Both horizontal and vertical
GridStyle.subtle        // Low opacity (10%)
GridStyle.dashed        // Dashed style
GridStyle.dotted        // Dotted style
```

**Line styles** (`GridLineStyle`): `solid`, `dashed`, `dotted`, `longDashed`.

### Adaptive labels

Labels adjust to the chart size by default:

```dart
InteractiveChart(
  candles: data,
  style: ChartStyle(
    adaptiveLabels: true,  // Default - auto-adjusts
  ),
)
```

- **Price labels:** about one label per 55 px of chart height, between 3 and 20.
- **Time labels:** spacing of 90 px by default.
- **Grid alignment:** grid lines always align with the labels.

Manual override:

```dart
InteractiveChart(
  candles: data,
  style: ChartStyle(
    adaptiveLabels: false,
    priceLabelCount: 7,      // Force 7 price labels
    timeLabelDensity: 100,   // One time label every 100px
  ),
)
```

`priceLabelCount` is clamped to 3-20 and `timeLabelDensity` to 60-120 pixels.

## Theming

Styling is done with `ChartStyle`. There are no built-in `dark()` or `light()` constructors, so build a style for each theme. `ChartStyle` has no background color field; set the background on the parent widget (for example a `Container` or the `Scaffold`).

```dart
ChartStyle(
  // Candle appearance
  candleBorderRadius: 4.0,
  priceGainColor: Colors.green,
  priceLossColor: Colors.red,

  // Volume
  showVolume: true,
  volumeColor: Colors.grey,
  volumeHeightFactor: 0.2,

  // Grid
  gridStyle: GridStyle.subtle,

  // Labels
  priceLabelStyle: TextStyle(color: Colors.white, fontSize: 12),
  timeLabelStyle: TextStyle(color: Colors.white, fontSize: 12),

  // Selection
  selectionHighlightColor: Colors.blue.withOpacity(0.2),
)
```

### ChartStyle reference

| Field | Default |
|---|---|
| `volumeHeightFactor` | `0.2` |
| `priceLabelWidth` | `48.0` |
| `timeLabelHeight` | `24.0` |
| `timeLabelStyle` | `TextStyle(fontSize: 16, color: Colors.grey)` |
| `priceLabelStyle` | `TextStyle(fontSize: 12, color: Colors.grey)` |
| `overlayTextStyle` | `TextStyle(fontSize: 16, color: Colors.white)` |
| `chartType` | `ChartType.candlestick` |
| `lineWidth` | `1.6` |
| `lineColor` | `null` (uses `priceGainColor`) |
| `priceGainColor` | `Colors.green` |
| `priceLossColor` | `Colors.red` |
| `showVolume` | `true` |
| `volumeColor` | `Colors.grey` |
| `trendLineStyles` | `[]` (list of `Paint` for `CandleData.trends`) |
| `gridStyle` | `GridStyle()` |
| `selectionHighlightColor` | `Color(0x33757575)` |
| `crosshairLabelColor` | `null` (opaque version of `selectionHighlightColor`) |
| `crosshairDashed` | `false` |
| `overlayBackgroundColor` | `Color(0xEE757575)` |
| `candleBorderRadius` | `0.0` |
| `adaptiveLabels` | `true` |
| `priceLabelCount` | `null` |
| `timeLabelDensity` | `null` |

`priceGridLineColor` is deprecated; use `gridStyle.horizontalGridColor`. `ChartStyle` and `PlayheadStyle` compare by value, so an unchanged style does not cause a repaint.

### Dark theme example

```dart
Container(
  color: const Color(0xFF131722),
  child: InteractiveChart(
    candles: candleData,
    style: ChartStyle(
      priceGainColor: const Color(0xFF26A69A),
      priceLossColor: const Color(0xFFEF5350),
      volumeColor: const Color(0xFF26A69A).withOpacity(0.3),
      timeLabelHeight: 32,
      priceLabelWidth: 60,
      overlayBackgroundColor: const Color(0xFF1E222D),
      overlayTextStyle: const TextStyle(color: Colors.white),
      selectionHighlightColor: Colors.blue.withOpacity(0.2),
    ),
  ),
)
```

### Overlay and indicator styles

Each indicator and overlay has its own style class: `RSIStyle`, `MACDStyle`, `BollingerBandsStyle`, `StochasticStyle`, `ATRStyle` and so on for indicators; `TradingLineStyle`, `PriceZoneStyle`, `FibonacciStyle` and `TrendLineStyle` for overlays.

## Controller and callbacks

### InteractiveChartController

Create one controller, pass it to the chart, and keep it in your `State`.

```dart
class ControlledChart extends StatefulWidget {
  @override
  _ControlledChartState createState() => _ControlledChartState();
}

class _ControlledChartState extends State<ControlledChart> {
  final _controller = InteractiveChartController();

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ElevatedButton(
          onPressed: () => _controller.jumpToLatest(),
          child: Text('Jump to Latest'),
        ),
        Expanded(
          child: InteractiveChart(
            candles: data,
            controller: _controller,
          ),
        ),
      ],
    );
  }
}
```

| Member | Description |
|---|---|
| `jumpToLatest()` | Scrolls to the most recent candle. |
| `seekToIndex(int candleIndex)` | Centers the visible range on a candle index (clamped; in replay mode the upper bound is `playheadIndex`). |
| `seekToTimestamp(int timestamp)` | Centers on the candle with the closest timestamp not exceeding the given one. |
| `setVisibleCandleCount(int count)` | Zooms: higher values zoom out, lower values zoom in. |
| `isAttached` | `true` when the controller is attached to a chart. |

The seek methods and `setVisibleCandleCount` do nothing if the controller is not attached.

### Manager events

All managers support event listeners. Each event carries a `type`, and the trading line event also has a nullable `line`:

```dart
// Trading Lines
lineManager.addListener((event) {
  switch (event.type) {
    case TradingLineEventType.added:
      print('Line added: ${event.line?.id}');
      break;
    case TradingLineEventType.priceChanged:
      print('Price changed: ${event.line?.price}');
      break;
    case TradingLineEventType.removed:
      print('Line removed');
      break;
    default:
      break;
  }
});

// Price Zones
zoneManager.addListener((event) {
  print('Zone event: ${event.type}');
});

// Fibonacci
fibManager.addListener((event) {
  print('Fibonacci event: ${event.type}');
});

// Trend Lines
trendLineManager.addListener((event) {
  print('Trend line event: ${event.type}');
});
```

Event types:

- `TradingLineEventType`: `added`, `removed`, `priceChanged`, `visibilityChanged`, `styleChanged`, `updated`, `cleared`
- `FibonacciEventType`: `added`, `removed`, `rangeChanged`, `visibilityChanged`, `styleChanged`, `updated`, `cleared`
- `TrendLineEventType`: `added`, `removed`, `pointsChanged`, `visibilityChanged`, `styleChanged`, `updated`, `cleared`

Each manager has `addListener`, `removeListener` and `clearListeners`. Remove listeners in `dispose()`.

### Serialization and syncing

Every overlay (and its style and options) provides `toJson()` / `fromJson()`. Each manager provides `serializeAll()` and `restoreAll()`. `ChartDrawingManager` combines the four managers with the standalone tools:

```dart
final drawings = ChartDrawingManager();

drawings.onDrawingChanged = (type, id, serialized) {
  // Send to your backend or another device
};

final json = drawings.serializeAll();   // Map<String, dynamic>
drawings.restoreAll(json);
```

`ChartDrawingManager` exposes `tradingLineManager`, `priceZoneManager`, `fibonacciManager` and `trendLineManager`, plus `add...` / `remove...` methods for `VerticalLine`, `TextTool`, `BrushTool`, `ArrowTool`, `CircleTool`, `RulerTool`, `GanttTool`, `PositionTool`, `FibonacciExtension` and `FibonacciFan`, and `clearAll()`, `totalCount`, `isEmpty` and `isNotEmpty`.

## Integration guide

### Recommended structure for a trading app

1. Keep the candle list and a controller in your `State` (or in your state-management layer).
2. Keep one manager per drawing family (`TradingLineManager`, `PriceZoneManager`, and so on), or a single `ChartDrawingManager`.
3. Pass `[...lineManager.lines, ...zoneManager.zones]` as `overlays`.
4. In each overlay callback (`onPriceChanged`, `onRangeChanged`, `onMoved`), update the manager and call `setState`. Persist the result to your backend if needed.
5. Use unique ids such as `'tool_${DateTime.now().millisecondsSinceEpoch}'`.

```dart
class _CompleteChartState extends State<CompleteChart> {
  final List<CandleData> _data = [];
  final TradingLineManager _lineManager = TradingLineManager();
  final PriceZoneManager _zoneManager = PriceZoneManager();

  bool _showRSI = true;
  bool _showMACD = false;

  @override
  void initState() {
    super.initState();
    _lineManager.addListener((event) => setState(() {}));
    _zoneManager.addListener((event) => setState(() {}));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: InteractiveChart(
        candles: _data,
        indicators: [
          if (_showRSI) RSIIndicator(period: 14),
          if (_showMACD) MACDIndicator(
            fastPeriod: 12,
            slowPeriod: 26,
            signalPeriod: 9,
          ),
        ],
        overlays: [
          ..._lineManager.lines,
          ..._zoneManager.zones,
        ],
        style: ChartStyle(
          priceGainColor: Colors.green,
          priceLossColor: Colors.red,
        ),
      ),
    );
  }

  @override
  void dispose() {
    _lineManager.clearListeners();
    _zoneManager.clearListeners();
    super.dispose();
  }
}
```

### Overlay update rule

An overlay that did not change should keep its instance, and a changed overlay must be a new instance. `TradingLine` is immutable, so build a new line (or use the manager's `updateLinePrice` / `updateLine`) whenever its text or price changes. The chart compares overlays and indicators by identity, item by item, which keeps repaints cheap when you rebuild with the same instances.

### Showing orders and positions

Use `TradingLine` with `TradingLineType.entry`, `stopLoss` and `takeProfit` for open orders, and `PositionTool` for a combined entry/SL/TP view. Pass an `onTap` to a non-draggable entry line to let users select the position on the chart.

```dart
TradingLine(
  id: 'entry_1',
  price: 150.0,
  type: TradingLineType.entry,
  options: TradingLineOptions(
    title: 'Entry',
    draggable: false,
    onTap: () {
      // Select the position in your UI
    },
  ),
)
```

### Streaming data over a WebSocket

The package does not include a network layer. Feed your own stream into the candle list. The sketch below uses the `web_socket_channel` package and assumes a feed that sends the current candle as JSON; adapt the URL and field names to your provider.

```dart
import 'dart:convert';
import 'package:web_socket_channel/web_socket_channel.dart';

class LiveChart extends StatefulWidget {
  const LiveChart({super.key});

  @override
  State<LiveChart> createState() => _LiveChartState();
}

class _LiveChartState extends State<LiveChart> {
  final List<CandleData> _candles = []; // seed with historical candles first
  final _controller = InteractiveChartController();
  late final WebSocketChannel _channel;

  @override
  void initState() {
    super.initState();
    _channel = WebSocketChannel.connect(Uri.parse('wss://your-feed.example/candles'));
    _channel.stream.listen((message) {
      final m = json.decode(message as String);
      final candle = CandleData(
        timestamp: m['timestamp'],
        open: (m['open'] as num).toDouble(),
        high: (m['high'] as num).toDouble(),
        low: (m['low'] as num).toDouble(),
        close: (m['close'] as num).toDouble(),
        volume: (m['volume'] as num).toDouble(),
      );
      setState(() {
        if (_candles.isNotEmpty && _candles.last.timestamp == candle.timestamp) {
          _candles[_candles.length - 1] = candle; // update the forming candle
        } else {
          _candles.add(candle);                    // a new candle started
        }
      });
    });
  }

  @override
  void dispose() {
    _channel.sink.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return InteractiveChart(
      candles: List.of(_candles),
      controller: _controller,
    );
  }
}
```

Handle reconnects, ordering and gaps in your own code, and validate data before building candles (for example, prices greater than zero and timestamps increasing).

### Best practices

- Always call `setState` after manager updates, and clear listeners in `dispose()`.
- Use unique ids for overlays.
- Limit the number of visible overlays (fewer than 50 is a good target) and use `visible: false` to hide instead of removing.
- Keep the candle window reasonable (the implementation guide suggests a maximum of about 500 candles loaded at once) and use [infinite history](#infinite-history-lazy-loading) for older data.
- Set `enableInteraction: false` for read-only or embedded charts, or when showing many charts at once.
- Handle errors when loading data.

## Migration from earlier versions

### Upgrading to 1.7.1

1.7.1 is a performance release. No public API was removed or made required, and charts written for 1.7.0 render identically. Notable behavior changes: trading line text layout is cached, far off-screen lines are culled, repaints compare items by identity, and `ChartStyle` and `PlayheadStyle` compare by value.

### Versions 1.3.0 to 1.7.0

These releases are additive (new parameters default to `null` or `false`):

- 1.7.0: `ChartType` and `ChartStyle.chartType`, `lineColor`, `lineWidth`, Heikin Ashi.
- 1.6.0: `onOverlayTapped`, `selectedOverlayId`, `onSelectedOverlayRectChanged`, crosshair beyond the data, `FractalsIndicator`.
- 1.5.0: always-on crosshair, `crosshairTimeLabel`, `onCrosshairActiveChanged`, `placement` / `onPlacementComplete`, price-axis scaling, denser price labels (about 55 px, up to 20).
- 1.4.0: replay mode. This version changed `InteractiveChartController.attach(...)` to named parameters; this only affects code that implements or calls `attach` itself.
- 1.3.0: `persistentCrosshair` and `ChartStyle.crosshairDashed`.

### Upgrading to 1.2.0 (breaking changes)

- `FibonacciOptions.onMoved` changed from `(double, double)` to `(double, double, int?, int?)`, adding `startTime` and `endTime`.
- `PriceZoneOptions.onRangeChanged` changed from `(double, double)` to `(double, double, int?, int?)`.
- `TapDetails.candle` is now nullable (`CandleData?`) and is `null` when tapping the future space.

New in 1.2.0: JSON serialization on all overlays, `serializeAll()` / `restoreAll()` on managers, `ChartDrawingManager`, `onDrawingChanged`, the `futureCandles` parameter, `TapDetails.timestamp` and `TapDetails.isFutureSpace`.

### Upgrading from 1.0.5 to 1.0.6 (grid)

`ChartStyle.priceGridLineColor` is deprecated. It still works but shows a deprecation warning.

```dart
// Before
InteractiveChart(
  candles: data,
  style: ChartStyle(
    priceGridLineColor: Colors.grey,  // Deprecated
  ),
)

// After
InteractiveChart(
  candles: data,
  style: ChartStyle(
    gridStyle: GridStyle(
      showHorizontalGrid: true,
      horizontalGridColor: Colors.grey.withOpacity(0.3),
    ),
  ),
)
```

### Upgrading from 1.0.3 to 1.0.4 (timestamp anchoring)

1.0.4 added optional `startTime` and `endTime` to `TradingLine`, `PriceZone` and `FibonacciRetracement`. This is backward compatible and fixes overlays that "stuck" to the viewport when scrolling.

```dart
TradingLine(
  price: 150.0,
  type: TradingLineType.stopLoss,
  startTime: startTimestamp,  // optional
  endTime: endTimestamp,      // optional
  options: TradingLineOptions(
    title: 'Stop Loss',
    draggable: true,
  ),
)
```

Without timestamps an overlay extends across the whole chart (the legacy behavior). Recommended: assign timestamps to overlays drawn by users, and omit them for global levels such as a global stop loss.

### Upgrading from the interactive_chart package or 1.0.0

Version 1.0.0 rebranded the project from `interactive_chart`. Manager classes require event listeners for drag and drop, and overlay ids must be provided for drag and drop to work.

## Examples

The repository includes a tabbed example app:

```bash
cd example
flutter run
```

The example contains tabs for:

- Indicators with toggles
- Trading lines
- Price zones
- Drawing tools (Fibonacci, trend lines and the other tools)
- Vertical zoom
- Infinite history (bidirectional lazy loading)
- Replay (play, pause, speed, scrub, bar and tick modes)
- Settings (grid, labels and candle controls)

Source files: `example/lib/tabbed_example.dart`, `replay_example.dart`, `infinite_history_example.dart` and `vertical_zoom_example.dart`.

### Complete minimal page

```dart
import 'package:flutter/material.dart';
import 'package:pipsend_charts/pipsend_charts.dart';

class BasicChart extends StatelessWidget {
  final List<CandleData> data = [
    CandleData(
      timestamp: DateTime.now().millisecondsSinceEpoch,
      open: 100.0,
      high: 105.0,
      low: 98.0,
      close: 103.0,
      volume: 1000000.0,
    ),
    // Add more candles (at least 3 are required)...
  ];

  @override
  Widget build(BuildContext context) {
    return InteractiveChart(
      candles: data,
      style: ChartStyle(
        priceGainColor: Colors.green,
        priceLossColor: Colors.red,
      ),
    );
  }
}
```

## Troubleshooting

### The chart throws an assertion on startup

`InteractiveChart` requires at least 3 candles, and `initialVisibleCandleCount` must be 3 or more. Show a loading state until your data has arrived. If you use replay, `playheadIndex` must be a valid index into `candles` and `playheadTickProgress` must be between 0 and 1.

### An overlay does not move

Check that:

1. `draggable: true` is set in the options.
2. The change callback (`onMoved`, `onPriceChanged`, `onRangeChanged`) is implemented and stores the new values.
3. `setState()` is called after the update, or the manager listener triggers a rebuild.
4. The overlay has a unique `id`.

### Overlays disappear or look wrong after a callback signature change

Since 1.2.0, `FibonacciOptions.onMoved` and `PriceZoneOptions.onRangeChanged` take four arguments (`..., int? startTime, int? endTime`). Update old two-argument callbacks.

### Text tool cannot be edited

Make sure `onEdit` is implemented, that the second tap arrives in under 300 ms, and that the callback returns the new text (or `null` to cancel).

### Grid lines are not visible

```dart
gridStyle: GridStyle(
  showHorizontalGrid: true,  // must be true
  showVerticalGrid: true,    // must be true
)
```

Increase the color opacity (for example `Colors.grey.withOpacity(0.5)`) or the stroke width, and make sure the color contrasts with your background.

### Too many or too few labels

Set `adaptiveLabels: false` and choose `priceLabelCount` (3-20) and `timeLabelDensity` (60-120).

### "setState() or markNeedsBuild() called during build"

This happens when `onXOffsetChanged` calls `setState` directly. Defer it:

```dart
onXOffsetChanged: (details) {
  if (details.isNearStart(50)) {
    Future.microtask(() => _loadMoreHistory());
  }
}
```

### The chart jumps when loading history

Prepend older candles (oldest first) instead of appending them:

```dart
_candles = [...newHistoricalData, ..._candles];
```

### Several history requests fire at once

Use a loading flag:

```dart
bool _isLoading = false;

Future<void> _loadMoreHistory() async {
  if (_isLoading) return;
  setState(() => _isLoading = true);
  try {
    // Load data
  } finally {
    setState(() => _isLoading = false);
  }
}
```

### Heikin Ashi overlays look off the candles

This is expected. Overlays keep their real prices, so with `ChartType.heikinAshi` they sit on the Heikin Ashi axis rather than on the original candles.

### Getting help

Open an issue or start a discussion on [GitHub](https://github.com/ArcaTechCo/PipsendChartsFl/issues).

## License

Pipsend Charts Flutter is distributed under a dual license. This section summarizes the `LICENSE` and `COMMERCIAL_LICENSE.md` files in the repository; it is not legal advice. Read the original files before relying on any term.

### Free license

The `LICENSE` file ("Pipsend Charts Flutter License", copyright 2025 Pipsend) grants permission "free of charge, to any person or organization" to use the software, subject to conditions.

**Permitted uses:**

- Use in personal projects (free)
- Use in commercial applications (free)
- Distribute the original, unmodified software
- Include the software as a dependency in your projects
- Educational use, research and development

**Restrictions:**

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

**Clarifications in the file:** you can use the unmodified software in commercial applications for free, and you can make internal modifications for your own use (not for redistribution), but you cannot sell or redistribute modified versions commercially. The license and copyright notice must be included in all copies or substantial portions of the software.

### Commercial license

A commercial license is required if you want to modify the software and distribute it commercially, create derivative works for commercial purposes, sell products based on modified versions, or redistribute modified versions under your own brand. `COMMERCIAL_LICENSE.md` lists these tiers:

| Tier | Price | Summary |
|---|---|---|
| Startup | $499 USD / year | Up to 5 developers, 1 commercial product, email support, 1 year of updates |
| Business | $1,999 USD / year | Up to 25 developers, up to 5 commercial products, priority email support, 1 year of updates |
| Enterprise | $4,999 USD / year | Unlimited developers and products, white-label and OEM rights, priority support, 1 year of updates |
| Perpetual | Contact for pricing | One-time payment, perpetual modification rights |

Per that file, you do not need a commercial license to use the unmodified library in a commercial app (including App Store and Play Store apps), for internal modifications, for personal projects, or for educational use. The file states that licenses are per organization, not per developer, and that final terms are provided upon purchase. To obtain a commercial license, contact Pipsend through [pipsend.com](https://pipsend.com).

### Third-party component

The package is based on [interactive_chart](https://github.com/MassimoTambu/interactive_chart) by Mehdi Zarepour (MIT License, copyright 2021). That component retains its original MIT license, which the `LICENSE` file reproduces.

### Warranty

The software is provided "as is", without warranty of any kind, and Pipsend and its contributors disclaim liability as set out in the `LICENSE` file.
