# 03. Custom canvas drawing

Walks through the [`custom_layer()`](https://quantchartsllc.com/docs/python/py-custom-draw.md#custom_layer) API. You will end with a `session_marks.py` indicator that draws a vertical dashed line at every N-minute boundary and a small text label at each one. This is the path to use when no built-in plot type fits: per-bar shapes, free-form rectangles, regime overlays.

The custom canvas system is a 2D drawing surface that runs after the chart paints. You build a layer, set a style, emit drawing operations (`move_to`, `line_to`, `rect`, `text`, ...), and call `.emit()` once. Coordinates are chart-space `(timestamp_ms, price)` by default, with viewport sentinels (`Y_MIN`, `Y_MAX`, `X_MIN`, `X_MAX`) that auto-resolve to the visible edges.

<a id="step-1-coordinate-spaces-and-sentinels"></a>
## Step 1. Coordinate spaces and sentinels

Drawing in chart space vs pixel space, and how the four viewport sentinels work.

`custom_layer` operations take tuples `(x, y)`. By default `x` is a timestamp in ms and `y` is a price.

When you want to anchor at the visible edges of the chart instead of a fixed price/time, use the sentinels: `Y_MIN` (bottom of visible price range), `Y_MAX` (top), `X_MIN` (left edge), `X_MAX` (right edge). They re-resolve every time the user pans or zooms, so a line drawn from `(ts, Y_MIN)` to `(ts, Y_MAX)` always spans the full visible price range.

### Notes

- Sentinels are simple sentinel constants, not functions. Just import them: `from quant_charts import Y_MIN, Y_MAX, X_MIN, X_MAX`.
- For pixel-space drawing (HUD-style overlays that ignore zoom), the layer supports a coord_space option. Most indicators stay in chart space.

<a id="step-2-layer-lifecycle"></a>
## Step 2. Layer lifecycle

Build, style, draw, emit. Each `custom_layer()` call returns a fresh CanvasLayer; you can have many per indicator (each with its own z-order).

### Example

```python
from quant_charts import custom_layer, Y_MIN, Y_MAX

layer = custom_layer("session-boundaries", z="top")
layer.style(stroke="#FFFFFF", line_width=1.0, alpha=0.3, dash=[4.0, 3.0])
# ... drawing ops ...
layer.emit()
```

### Notes

- The first arg is a unique layer name within the indicator. Reusing the name in a re-run replaces the previous layer.
- `z="top"` draws above candles. `z="bottom"` draws under them.
- `.style()` sets defaults the layer reuses for every shape. Call again to switch mid-stream.

<a id="step-3-drawing-primitives"></a>
## Step 3. Drawing primitives

The set of operations a layer supports.

Path-style: `.begin()` opens a path, `.move_to((x, y))` jumps without drawing, `.line_to((x, y))` extends, `.close()` closes the path back to the start, `.stroke()` strokes it with the current style, `.fill()` fills it.

Shape-style: `.rect((x1, y1), (x2, y2))` for a rectangle, `.arc((cx, cy), radius, start_rad, end_rad)` for an arc.

Text: `.text((x, y), "label", font_size=11, color="#fff", align="center")`.

### Example

```python
# Vertical line
layer.begin().move_to((ts, Y_MIN)).line_to((ts, Y_MAX)).stroke()

# Filled rectangle (highlight a session)
layer.style(fill="#7aa2f720")
layer.rect((session_start, Y_MIN), (session_end, Y_MAX)).fill()

# Text label at the top of the chart
layer.text((ts, Y_MAX), "RTH", font_size=10, color="#a1a1aa", align="center")
```

<a id="step-4-the-full-session_marks-indicator"></a>
## Step 4. The full session_marks indicator

Vertical lines every N minutes, with the time as a label. Save as `session_marks.py` under `indicators/`.

### Example

```python
import numpy as np
from quant_charts import indicator, input, custom_layer, Y_MIN, Y_MAX


@indicator(
    name="Session Marks",
    description="Vertical lines + time labels at every N-minute boundary.",
    overlay=True,
    data_mode="ohlc",
    required_columns=["timestamp"],
)
class SessionMarks:
    minutes = input.int(15, "Minutes per mark", min=1, max=240)
    line_color = input.color("#a1a1aa", "Line Color")
    label_color = input.color("#c5c5cd", "Label Color")
    show_labels = input.bool(True, "Show Labels")

    def calculate(self, df):
        ts = np.asarray(df["timestamp"], dtype=np.int64)
        if ts.size == 0:
            return

        bucket_ms = self.minutes * 60_000
        floored = (ts // bucket_ms) * bucket_ms
        mask = np.empty(ts.size, dtype=bool)
        mask[0] = True
        mask[1:] = floored[1:] != floored[:-1]
        marks = floored[mask]

        layer = custom_layer("session-marks", z="bottom")
        layer.style(stroke=self.line_color, line_width=1.0, alpha=0.35, dash=[4.0, 3.0])
        for ms in marks.tolist():
            layer.begin().move_to((int(ms), Y_MIN)).line_to((int(ms), Y_MAX)).stroke()

        if self.show_labels:
            label_layer = custom_layer("session-marks-labels", z="top")
            for ms in marks.tolist():
                # ms -> HH:MM in ET; cheap formatter (no datetime dep)
                seconds = (int(ms) // 1000) % 86400
                hh = (seconds // 3600) % 24
                mm = (seconds // 60) % 60
                label = f"{hh:02d}:{mm:02d}"
                label_layer.text((int(ms), Y_MAX), label,
                                 font_size=10, color=self.label_color, align="center")
```

### Notes

- Two layers (`session-marks` and `session-marks-labels`) so lines can sit below candles while labels float above.
- `Y_MIN`/`Y_MAX` re-resolve every paint, so the lines auto-fit to whatever zoom level the user is on.
- No dataframe column added, no [`plot()`](https://quantchartsllc.com/docs/python/py-plotting.md#plot) call: the layer is the only output of this indicator.

<a id="step-5-run-it"></a>
## Step 5. Run it

Save, validate, attach.

2. Open the Indicator panel, click Add, select "Session Marks".
3. Try minutes=5, 15, 60. Toggle labels off for a cleaner view at small timeframes.

### Notes

- If the chart looks unchanged, check the in-app terminal panel for validation errors. A common one is forgetting `Y_MIN`/`Y_MAX` import.
- Custom layers are part of the indicator output, so all the analyzer-side caching, sweep parallelism, and re-run paths work the same as for `plot()` indicators.
