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

# Common drawing patterns

This guide provides ready-to-use solutions for common drawing tasks in Indie indicators. Each pattern addresses a specific visualization need with working code examples.

## Key Concepts for Drawing in Indie

Before diving into patterns, let's recall these essential concepts that affect how drawings work in Indie.

### Two Execution Phases: History vs Realtime

Indie indicators run in two distinct phases:

1. **History calculation**: Each bar is processed once sequentially from left to right. Drawing objects are created and remain stable.
2. **Realtime updates**: When new intrabar data arrives, the indicator re-executes for the current bar multiple times.

> [!NOTE]
>
> Code that works perfectly during history calculation can break during realtime updates. Always consider both phases when writing drawing code, and test your indicators on instruments with realtime updates on 1m timeframe for several minutes to ensure runtime stability.

### Chart State Rollbacks

During realtime updates, when new intrabar data arrives:

1. The chart state rolls back to what it was at the **beginning of the current bar**
2. Your indicator's `calc()` function executes again with updated data

> [!NOTE]
>
> If you created a drawing during an intrabar update, the rollback will remove it. If you erased a drawing during an intrabar update, the rollback will restore it.

### Why Series and Var exist

`MutSeries[T]` and `Var[T]` do two things for you: they carry a value from one bar to the
next, and they roll it back on every intrabar update. The first is why function-based
indicators, which have no class fields to keep anything in, reach for a `Var` at all. The
second is the part that is easy to miss: inside one candle your code runs many times, and
usually only the final result of that candle should survive — not a value accumulated over
every update within it. So before each re-execution both containers restore the value the
candle started with. You do not manage that — you read the value with `get()` and it is
already the right one.

The difference between the two is history: a `MutSeries[T]` also keeps the values of previous
candles and lets you read them with `[1]`, `[2]` and so on, while a `Var[T]` keeps only the
latest state.

A class field gives you the first half and not the second: it persists, but it keeps whatever
the previous intrabar update left in it.

### When a drawing belongs in a Var

A drawing is an object like any other, so the same rule applies — with the chart added to
the picture, because the chart also rolls its own state back to the start of the candle
before each re-execution. Put a drawing in a `Var` when:

* **The drawing's next state depends on its current state.** Pattern 4 below starts each new
  polyline segment from the previous segment's endpoint. The chart rolled that segment back;
  a plain class field did not, so the two disagree and the polyline breaks.
* **You read values out of the drawing** for anything else, and need them to be the values
  the chart is showing right now.

> [!NOTE]
>
> State you accumulate yourself — a bar counter, a running total — belongs in a `Var` or a
> `MutSeries` whether a drawing displays it or not. In a plain class field it counts intrabar
> updates instead of candles, because nothing undoes the previous update's work. Once the
> number is in a container, the label that displays it can stay in a plain class field.

> [!NOTE]
>
> What a `Var` does **not** do is make erasing safe. It cannot reattach a drawing that an
> intrabar rollback detached from the chart — and it does not need to:
> [`self.chart.erase()`](https://takeprofit.com/docs/indie/Library-reference/package-indie-drawings#method_Chart_erase) on
> an object that is not currently on the chart does nothing at all. You can erase without
> knowing whether the drawing is there.

**Simple guidelines:**

1. **One-time drawings** (labels on conditions): just create and draw — no special handling
2. **Persistent drawings** (info panels): create the object once and reuse it, in a class field or a `Var`
3. **Drawings you read back** (polylines, anything whose next state is built from its current one): keep in a `Var` and reach it through `get()`

### Reach the drawing through get()

`set()` hands the value over to the container, which stores a copy of the drawing's fields.
Edits you make to your local object afterwards do not reach it. Keep a single handle — the
`Var` — and go through `get()` for every draw, edit and erase.

```py theme={null}
# Avoid: two handles, and the one you keep editing is not the one you draw
line = LineSegment(point_a, point_b)
line_var.set(line)
line.color = color.RED            # the Var's line keeps its original color
self.chart.draw(line_var.get())

# Better: one handle
line_var.set(LineSegment(point_a, point_b))
line_var.get().color = color.RED
self.chart.draw(line_var.get())
```

> [!NOTE]
>
> The same applies to later edits: change the drawing through `my_var.get()` (e.g.
> `my_var.get().text = '...'`), not through a local variable you kept on the side.

## Common Drawing Patterns

### Pattern 1: Draw labels when conditions are met

**Problem**: You want to display labels on specific bars when certain conditions are satisfied, such as price patterns, indicator signals, or periodic intervals.

**Solution**: Use conditional logic to check your criteria and create new label objects when conditions are met. No special object management is required.

```py Label breakouts and periodic bars theme={null}
# indie:lang_version = 5
from indie import indicator, color
from indie.drawings import LabelAbs, AbsolutePosition
from indie.algorithms import Highest

@indicator('Conditional Labels', overlay_main_pane=True)
def Main(self):
    # Example 1: Label every 5th bar
    if self.bar_index % 5 == 0:
        self.chart.draw(LabelAbs(
            'Bar ' + str(self.bar_index),
            AbsolutePosition(self.time[0], self.high[0]),
        ))

    # Example 2: Label price breakouts
    highest_20 = Highest.new(self.high, 20)
    if self.close[0] > highest_20[1]:
        self.chart.draw(LabelAbs(
            "BREAKOUT!",
            AbsolutePosition(self.time[0], self.close[0]),
            bg_color=color.BLUE,
        ))
```

**How it works**: Labels are created when conditions are met and automatically persist on the chart. During realtime updates, the chart rolls back and re-executes, creating labels again when conditions are still true.

![Figure 1. Conditional Labels.](https://mintcdn.com/takeprofit-a5dc0462/G8Lo_-_8Elf7cBMe/images/indie/Plotting-and-drawing/common-drawings-1.png?fit=max\&auto=format\&n=G8Lo_-_8Elf7cBMe\&q=85\&s=92392e84087bf95e5c529327e56bf17e)

### Pattern 2: Display information in fixed screen position

**Problem**: You want to show live statistics, indicator values, or other information that stays in a fixed position on screen regardless of chart scrolling or zooming.

**Solution**: Create a `LabelRel` object once and reuse it by updating its text property. Store the label in a class field (for class-based indicators) or use `Var` (for function-based indicators).

```py Live stats panel [function-based] theme={null}
# indie:lang_version = 5
from indie import indicator, Var
from indie.drawings import LabelRel, RelativePosition, vertical_anchor as va, horizontal_anchor as ha

@indicator('Info Panel', overlay_main_pane=True)
def Main(self):
    # Create label once, position it in top-right corner
    info_label_var = Var[LabelRel].new(LabelRel(
        "Initializing...",
        RelativePosition(va.TOP, ha.RIGHT, 0.1, 0.9)
    ))

    # Update content with current data
    info_label_var.get().text = "Price: " + str(self.close[0]) + "\nBar: " + str(self.bar_count)

    # Redraw to update display
    self.chart.draw(info_label_var.get())
```

```py Live stats panel [class-based] theme={null}
# indie:lang_version = 5
from indie import indicator, MainContext
from indie.drawings import LabelRel, RelativePosition, vertical_anchor as va, horizontal_anchor as ha

@indicator('Info Panel', overlay_main_pane=True)
class Main(MainContext):
    def __init__(self):
        # Create label once, position it in top-right corner
        self.info_label = LabelRel(
            "Initializing...",
            RelativePosition(va.TOP, ha.RIGHT, 0.1, 0.9)
        )

    def calc(self):
        # Update content with current data
        self.info_label.text = "Price: " + str(self.close[0]) + "\nBar: " + str(self.bar_count)

        # Redraw to update display
        self.chart.draw(self.info_label)
```

> [!NOTE]
>
> Always reuse the same `LabelRel` object. Creating new relative-positioned labels on each bar will cause an error "count of relative positioned drawings exceeded the limit 100".

![Figure 2. Info Panel.](https://mintcdn.com/takeprofit-a5dc0462/G8Lo_-_8Elf7cBMe/images/indie/Plotting-and-drawing/common-drawings-2.png?fit=max\&auto=format\&n=G8Lo_-_8Elf7cBMe\&q=85\&s=b5d9e5fd09988d43a34b0300ac79466c)

### Pattern 3: Label that follows the current bar

**Problem**: You want a label that appears only above the most recent bar and moves with it as new bars arrive, displaying current values or active signals.

**Solution**: Create a `LabelAbs` object once and update both its position and text properties on each bar to follow the current price action.

```py Current price tracker [function-based] theme={null}
# indie:lang_version = 5
from indie import indicator, Var
from indie.drawings import LabelAbs, AbsolutePosition

@indicator('Current Bar Tracker', overlay_main_pane=True)
def Main(self):
    # Create label once with dummy position
    tracker_var = Var[LabelAbs].new(LabelAbs("Initializing", AbsolutePosition(0, 0)))

    # Calculate what to display
    price_change = self.close[0] - self.close[1]
    change_pct = (price_change / self.close[1]) * 100

    # Update both text and position
    tracker_var.get().text = str(round(change_pct, 3)) + '%'
    tracker_var.get().position = AbsolutePosition(self.time[0], self.high[0])

    # Redraw at new position
    self.chart.draw(tracker_var.get())
```

```py Current price tracker [class-based] theme={null}
# indie:lang_version = 5
from indie import indicator, MainContext
from indie.drawings import LabelAbs, AbsolutePosition

@indicator('Current Bar Tracker', overlay_main_pane=True)
class Main(MainContext):
    def __init__(self):
        # Create label once with dummy position
        self.tracker = LabelAbs("Initializing", AbsolutePosition(0, 0))

    def calc(self):
        # Calculate what to display
        price_change = self.close[0] - self.close[1]
        change_pct = (price_change / self.close[1]) * 100

        # Update both text and position
        self.tracker.text = str(round(change_pct, 3)) + '%'
        self.tracker.position = AbsolutePosition(self.time[0], self.high[0])

        # Redraw at new position
        self.chart.draw(self.tracker)
```

**Key point**: By reusing the same label object and updating its position, you ensure only one label exists at any time, creating a smooth tracking effect.

![Figure 3. Current Bar Tracker.](https://mintcdn.com/takeprofit-a5dc0462/G8Lo_-_8Elf7cBMe/images/indie/Plotting-and-drawing/common-drawings-3.png?fit=max\&auto=format\&n=G8Lo_-_8Elf7cBMe\&q=85\&s=88b297648cf09c8f9582d2c7c917aa62)

### Pattern 4: Draw polyline connecting specific points

**Problem**: You want to create connected lines that pass through specific price points (e.g., highs every 5 bars), forming a polyline that updates as new bars arrive.

**Solution**: Maintain only the current active segment using `Var`. Update the segment's endpoint between anchor points, and create new segments at anchor points.

```py Connect highs every 5 bars [function-based] theme={null}
# indie:lang_version = 5
from indie import indicator, Var, Optional
from indie.drawings import LineSegment, AbsolutePosition

@indicator('Polyline Pattern', overlay_main_pane=True)
def Main(self):
    # Store current segment in Var so it rolls back with the chart
    current_segment = Var[Optional[LineSegment]].new(None)

    # Current point (using high for visibility)
    curr_point = AbsolutePosition(self.time[0], self.high[0])

    # Every 5th bar is an anchor point
    if self.bar_index % 5 == 0:
        start_point = curr_point  # Default for first segment

        # Continue from previous segment's endpoint if it exists
        if current_segment.get() is not None:
            start_point = current_segment.get().value().point_b

        # Create new segment
        current_segment.set(LineSegment(start_point, curr_point))

    # Between anchors: update current segment's endpoint
    elif current_segment.get() is not None:
        current_segment.get().value().point_b = curr_point

    # Draw the active segment
    if current_segment.get() is not None:
        self.chart.draw(current_segment.get().value())
```

```py Connect highs every 5 bars [class-based] theme={null}
# indie:lang_version = 5
from indie import indicator, Var, Optional, MainContext
from indie.drawings import LineSegment, AbsolutePosition

@indicator('Polyline Pattern', overlay_main_pane=True)
class Main(MainContext):
    def __init__(self):
        # Store current segment in Var so it rolls back with the chart
        self.current_segment = self.new_var(Optional[LineSegment]())

    def calc(self):
        # Current point (using high for visibility)
        curr_point = AbsolutePosition(self.time[0], self.high[0])

        # Every 5th bar is an anchor point
        if self.bar_index % 5 == 0:
            start_point = curr_point  # Default for first segment

            # Continue from previous segment's endpoint if it exists
            if self.current_segment.get() is not None:
                start_point = self.current_segment.get().value().point_b

            # Create new segment
            self.current_segment.set(LineSegment(start_point, curr_point))

        # Between anchors: update current segment's endpoint
        elif self.current_segment.get() is not None:
            self.current_segment.get().value().point_b = curr_point

        # Draw the active segment
        if self.current_segment.get() is not None:
            self.chart.draw(self.current_segment.get().value())
```

**Why Var is essential**: this is the read-back case. Each new segment starts from the previous segment's endpoint, so the value read out of the drawing has to be the one the chart shows — and the chart rolls its state back on every intrabar update. The `Var` makes the segment roll back with it. The `Optional` is a separate thing: it only expresses "there is no segment yet", before the first anchor bar.

![Figure 4. Polyline Pattern.](https://mintcdn.com/takeprofit-a5dc0462/G8Lo_-_8Elf7cBMe/images/indie/Plotting-and-drawing/common-drawings-4.png?fit=max\&auto=format\&n=G8Lo_-_8Elf7cBMe\&q=85\&s=88bff8c3f325155d90da592783bf0ba2)

**What happens without Var**: If you used a regular class field `self.current_segment: Optional[LineSegment]` instead:

1. On anchor bar (e.g., bar 10), you create a new segment starting from the previous segment's endpoint and save it to `self.current_segment`
2. An intrabar update arrives, chart rolls back to the start of bar 10
3. During recalculation, the code tries to create a new LineSegment, but takes its start point from `self.current_segment`, which was modified during the previous update and wasn't rolled back
4. This creates a disconnected polyline — the new segment starts from an incorrect position

### Pattern 5: Erase drawings created on previous executions

**Problem**: You need to erase drawings that were created during previous bar updates or
previous intrabar executions. This is common for temporary overlays, alerts, or any drawing
that should appear and disappear based on conditions.

**Solution**: Let's explore this through a concrete example — a blinking line indicator. The
instructive part is the first attempt, because its failure is silent.

**First attempt — a new object in a class field:**

```py theme={null}
# indie:lang_version = 5
from indie import indicator, Optional, MainContext
from indie.drawings import LineSegment, AbsolutePosition

@indicator('Blinking Line - Broken', overlay_main_pane=True)
class Main(MainContext):
    def __init__(self):
        self.is_visible = True
        self.line_segment: Optional[LineSegment] = None

    def calc(self):
        self.is_visible = not self.is_visible

        if self.is_visible:
            self.line_segment = LineSegment(
                AbsolutePosition(self.time[1], self.close[1]),
                AbsolutePosition(self.time[0], self.close[0]),
            )
            self.chart.draw(self.line_segment.value())
        elif self.line_segment is not None:
            self.chart.erase(self.line_segment.value())  # Erases nothing!
```

**Where it breaks**: Consider this scenario:

1. On the last update of bar N, the line is erased (`is_visible` was False)
2. Bar N+1 starts with no line on the chart
3. First execution: `is_visible` becomes True, a new line object is created and drawn
4. Intrabar update arrives, chart rolls back to start of bar N+1 (no line)
5. Second execution: `is_visible` becomes False, so the script erases the object the class field still points at — an object the rollback has already detached from the chart
6. The erase does nothing: the object the field points at is not the object the chart is showing

**What you end up seeing** depends on the phasing. Shift the scenario by one update — the
candle begins with the line already on the chart — and the erase in step 5 does land, the
rollback then puts the line back, and the next execution draws a *second* line. Over a run
the two cases mix, so the chart accumulates lines the script is certain it removed.

> [!NOTE]
>
> Nothing is reported when this happens — the erase is simply skipped. And it only happens
> during realtime, so there is nothing to point at the cause.

**The working solution — one object, stated explicitly on every execution:**

```py theme={null}
# indie:lang_version = 5
from indie import indicator, MainContext
from indie.drawings import LineSegment, AbsolutePosition

@indicator('Blinking Line', overlay_main_pane=True)
class Main(MainContext):
    def __init__(self):
        self.is_visible = True
        # One line object for the whole life of the indicator
        self.line = LineSegment(AbsolutePosition(0, 0), AbsolutePosition(0, 0))

    def calc(self):
        self.is_visible = not self.is_visible

        if self.is_visible:
            self.line.point_a = AbsolutePosition(self.time[1], self.close[1])
            self.line.point_b = AbsolutePosition(self.time[0], self.close[0])
            self.chart.draw(self.line)
        else:
            self.chart.erase(self.line)
```

**Why this works**: the problem in the first attempt was never the class field — it was the
new object on every execution. One object cannot get out of sync with the chart, because
every execution ends by stating what that one object should be: on the chart, or off it.
Whatever a rollback did before that, the candle ends in the state the last execution asked
for. And nothing has to be tracked: `erase` on a line that is not on the chart does nothing,
and `draw` on a line that is already there updates it.

Note that there is no `Var` here, and no `Optional` standing in for "not drawn". Neither is
needed: nothing is ever read back out of the line, and both of its endpoints are overwritten
from current bar data each time, so there is no state to roll back. Compare Pattern 4, where
each segment *is* built from the previous one's endpoint — that is what makes the `Var`
load-bearing there.

**Result**: The line blinks with each execution — appearing when `is_visible` is true and
disappearing when false. During realtime updates, each intrabar update toggles the
visibility, creating a visual heartbeat of market activity. The toggle lives in a plain class
field on purpose: it is meant to flip on every execution, intrabar updates included, which is
exactly what the "heartbeat" is.

> [!NOTE]
>
> Reusing one object also keeps the number of chart objects flat. Over the same data, this
> indicator creates exactly one, while a version that builds a fresh `LineSegment` on each
> toggle creates one per toggle — a count that keeps growing for as long as the indicator runs.

**When you cannot reuse a single object**: some indicators draw a number of objects that
varies — a set of zones, a pool of levels. Give them a fixed set of reused objects and erase
the ones that should disappear; you do not need to track which are currently drawn, because
erasing one that is not on the chart is a no-op.

### Pattern 6: Skip redundant draws during history with `is_last_bar`

**Problem**: You have a drawing that gets updated every bar (e.g., an info panel, a "current price" label) but only the final state matters to the user. During history calculation the runtime tracks every `chart.draw()` call internally, so redrawing the same object on each of thousands of history bars wastes memory and slows down calculation — even though only the last bar's state will ever be visible.

**Solution**: Guard the drawing call with `self.is_last_bar` so that the object is only drawn once — on the most recent bar. During realtime updates `is_last_bar` is always `True`, so the drawing updates normally.

```py Dashboard drawn only on last bar [function-based] theme={null}
# indie:lang_version = 5
from indie import indicator, Var
from indie.drawings import LabelRel, RelativePosition, vertical_anchor as va, horizontal_anchor as ha

@indicator('Efficient Dashboard', overlay_main_pane=True)
def Main(self):
    # Create the label object once via Var
    info_panel = Var[LabelRel].new(LabelRel(
        '',
        RelativePosition(va.TOP, ha.RIGHT, 0.1, 0.9),
    ))

    # All indicator logic runs on every bar as usual
    price_change = self.close[0] - self.close[1]
    change_pct = (price_change / self.close[1]) * 100

    # Only draw on the last bar — skips thousands of redundant draws during history
    if self.is_last_bar:
        info_panel.get().text = 'Change: ' + str(round(change_pct, 3)) + '%'
        self.chart.draw(info_panel.get())
```

```py Dashboard drawn only on last bar [class-based] theme={null}
# indie:lang_version = 5
from indie import indicator, MainContext
from indie.drawings import LabelRel, RelativePosition, vertical_anchor as va, horizontal_anchor as ha

@indicator('Efficient Dashboard', overlay_main_pane=True)
class Main(MainContext):
    def __init__(self):
        self.info_panel = LabelRel(
            '',
            RelativePosition(va.TOP, ha.RIGHT, 0.1, 0.9),
        )

    def calc(self):
        # All indicator logic runs on every bar as usual
        price_change = self.close[0] - self.close[1]
        change_pct = (price_change / self.close[1]) * 100

        # Only draw on the last bar — skips thousands of redundant draws during history
        if self.is_last_bar:
            self.info_panel.text = 'Change: ' + str(round(change_pct, 3)) + '%'
            self.chart.draw(self.info_panel)
```

**When to use this pattern**: Apply it to any drawing that is redrawn on every bar with only the latest state being meaningful — dashboards, info panels, "current value" trackers, trailing stop lines, etc. Do **not** use it for drawings that should remain on the chart at their historical positions (e.g., signal labels, trend lines between pivot points).

### Pattern 7: Prefer `@level` over a constant plot line

**Problem**: You need a fixed horizontal line at a known value — an overbought threshold, a zero line, a target price. It is tempting to reach for `@plot.line` and return the constant on every bar, but that is the wrong tool for a static value.

**Solution**: Use the [`@level`](https://takeprofit.com/docs/indie/Plotting-and-drawing/Fills-levels-and-bands#levels) decorator. It is a purely static, compile-time decorator: it takes no return value and occupies no slot in the return tuple. The line is described once, at compile time, from a single constant.

```py Efficient — @level theme={null}
# indie:lang_version = 5
from indie import indicator, plot, level, color
from indie.algorithms import Rsi

@indicator('RSI with level')
@plot.line('rsi')
@level(70, line_color=color.RED)
def Main(self):
    return Rsi.new(self.close, 14)[0]
```

```py Wasteful — constant plot line theme={null}
# indie:lang_version = 5
from indie import indicator, plot, color
from indie.algorithms import Rsi

@indicator('RSI with const line')
@plot.line('rsi')
@plot.line('ob', color=color.RED)
def Main(self):
    # 70.0 is re-evaluated, stored, and streamed on EVERY bar
    return Rsi.new(self.close, 14)[0], 70.0
```

**Why `@level` is faster and leaner**: A value returned from `@plot.line` is treated as a full time series. The runtime evaluates it, stores it, and streams it to the chart on every one of the thousands of history bars — even though the number never changes. `@level` carries a single constant: nothing is computed or stored per bar. Same visual result, a fraction of the memory and compute.

### Pattern 8: Prefer `@band` over two lines with a fill

**Problem**: You want a shaded zone between two fixed values — a neutral RSI zone, a value area, a no-trade corridor. The manual approach is two `@plot.line` decorators plus a `@plot.fill`, returning both constants and a `plot.Fill()` every bar.

**Solution**: Use the [`@band`](https://takeprofit.com/docs/indie/Plotting-and-drawing/Fills-levels-and-bands#bands) decorator. Like `@level`, it is static and compile-time: two constant edges and a fill described once, with no return value and no output slots to feed.

```py Efficient — @band theme={null}
# indie:lang_version = 5
from indie import indicator, plot, band, color
from indie.algorithms import Rsi

@indicator('RSI with band')
@plot.line('rsi')
@band(70, 30, fill_color=color.GRAY(0.1))
def Main(self):
    return Rsi.new(self.close, 14)[0]
```

```py Wasteful — two lines + fill theme={null}
# indie:lang_version = 5
from indie import indicator, plot, color
from indie.algorithms import Rsi

@indicator('RSI with two lines and fill')
@plot.line('rsi')
@plot.line('ob', color=color.GRAY)
@plot.line('os', color=color.GRAY)
@plot.fill('ob', 'os', color=color.GRAY(0.1))
def Main(self):
    # Two constants and a Fill returned on EVERY bar
    return Rsi.new(self.close, 14)[0], 70.0, 30.0, plot.Fill()
```

**Why `@band` is faster and leaner**: The manual version occupies three extra output slots — two constant lines and a fill — each tracked as its own per-bar series across the entire history. `@band` replaces all three with a single static declaration that carries two constants. Fewer output slots, no per-bar evaluation, less data streamed to the chart.

> [!NOTE]
>
> **Rule of thumb**: whenever a horizontal line or zone sits at a value that is known at compile time and never changes during the run, use `@level` or `@band`. Reach for a constant `@plot.line` (and `@plot.fill`) only when the value is genuinely dynamic — computed at runtime or changing bar-to-bar (e.g., a volatility-adjusted threshold). In that case the value really is a series, and a plot is the right tool.


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