# Custom Canvas

Free-form 2D canvas overlays via custom_layer(...). For visuals no predefined plot type covers: TF boundaries, R-multiple labels, candle-style overlays, custom histograms.

<a id="custom_layer"></a>
## custom_layer

```python
custom_layer(name, z="normal")
```

Build a free-form 2D canvas overlay on the chart.

Returns a [`CanvasLayer`](https://quantchartsllc.com/docs/rust/rust-custom-draw.md#canvaslayer) builder. Chain `.style(...).begin().move_to(...).line_to(...).stroke()` (etc.) and finish with `.emit()`. Each call to `emit()` registers the layer with the per-calculation buffer; the indicator wrapper picks it up automatically.

Two coordinate spaces:
- **chart**: tuples `(ts_ms, price)` get resolved through `timeToX / priceToY` so pan/zoom respects them.
- **pixel**: raw CSS pixels for HUDs that stay glued to the viewport.

Viewport-edge sentinels (`Y_MIN`, `Y_MAX`, `X_MIN`, `X_MAX`) save you from knowing the price scale - drop them into the y component to span the visible price range.

Use this when none of the predefined plot types fit: candle-style overlays, custom histograms with arbitrary geometry, text annotations, vertical TF boundaries, R-multiple labels above trades, anything 2D.

### Parameters

- `name` (`str`, default `required`): Unique key for the layer; same name in two indicators will collide.
- `z` (`str`, default `"normal"`): `bottom`, `normal`, or `top`. Controls draw order vs. price series.

### Returns

CanvasLayer

### Example

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

@indicator("TF Boundaries", overlay=True, data_mode="ohlc")
class TfBoundaries:
    minutes = input.int(1, "Boundary Minutes", min=1, max=60)
    def calculate(self, df):
        ts = np.asarray(df["timestamp"], dtype=np.int64)
        bucket = self.minutes * 60_000
        floored = (ts // bucket) * bucket
        boundaries = floored[np.r_[True, floored[1:] != floored[:-1]]]

        layer = custom_layer("tf-boundaries", z="top")
        layer.style(stroke="#ffffff44", line_width=1.0, dash=[4, 3])
        for ms in boundaries:
            layer.begin().move_to((int(ms), Y_MIN)).line_to((int(ms), Y_MAX)).stroke()
        layer.emit()
```

### Notes

- `emit()` MUST be called or the layer is dropped silently.
- Calling `emit()` twice on the same layer registers it twice - almost always a bug.
- Hard cap of 50,000 ops per layer at the renderer; unknown ops are ignored.

<a id="canvaslayer-style"></a>
## CanvasLayer.style

```python
CanvasLayer.style(*, fill?, stroke?, line_width?, alpha?, font?, text_align?, text_baseline?, dash?)
```

Set sticky drawing style. Call before path ops or between strokes.

Style is sticky: every following stroke/fill/text uses the most recent values. Calling `style()` BEFORE the first path op folds into the layer defaults; after that, each call emits an explicit `setStyle` op so style changes mid-sequence work.

`alpha` is clamped to 0.0-1.0. `dash` is a `[on, off]` pixel array per HTML5 canvas convention.

### Parameters

- [`fill`](https://quantchartsllc.com/docs/python/py-plotting.md#fill) (`str`, default `None`): Fill color (hex). Used by `fill()` and `text()` (when `stroke=False`).
- `stroke` (`str`, default `None`): Stroke color (hex). Used by `stroke()` and `text(stroke=True)`.
- `line_width` (`float`, default `None`): Line thickness in CSS pixels.
- `alpha` (`float`, default `None`): Global alpha 0.0-1.0. Compounds with hex alpha.
- `font` (`str`, default `None`): CSS font shorthand, e.g. `"12px Inter"`.
- `text_align` (`str`, default `None`): `left`, `center`, `right`, `start`, `end`.
- `text_baseline` (`str`, default `None`): `top`, `middle`, `bottom`, `alphabetic`, `hanging`.
- `dash` (`list[float]`, default `None`): Dash pattern as `[on_px, off_px, ...]`. `[]` = solid.

### Returns

CanvasLayer (chainable)

### Example

```python
layer.style(stroke="#7aa2f7", line_width=2.0, dash=[4, 4])
layer.begin().move_to((ts0, p0)).line_to((ts1, p1)).stroke()
layer.style(stroke="#e0af68")  # mid-sequence change emits setStyle op
layer.begin().move_to((ts1, p1)).line_to((ts2, p2)).stroke()
```

<a id="canvaslayer-move_to-line_to"></a>
## CanvasLayer.move_to / line_to

```python
CanvasLayer.move_to / line_to(point, *, space="chart")
```

Move pen / draw line to a point.

`point` is `(ts_ms, price)` in chart space, or `(x_px, y_px)` in pixel space. Strings select viewport sentinels: `Y_MIN`/`Y_MAX` on the y component span the visible price range; `X_MIN`/`X_MAX` on the x component span the visible time range.

### Parameters

- `point` (`tuple`, default `required`): `(x, y)` tuple. x is ms or px; y is price or px; either component accepts a sentinel string.
- `space` (`str`, default `"chart"`): `chart` or `pixel`.

### Returns

CanvasLayer (chainable)

### Example

```python
# vertical line from bottom to top of viewport at ts_ms
layer.begin()
layer.move_to((ts_ms, Y_MIN))
layer.line_to((ts_ms, Y_MAX))
layer.stroke()
```

<a id="canvaslayer-rect"></a>
## CanvasLayer.rect

```python
CanvasLayer.rect(top_left, w_px, h_px, *, space="chart")
```

Draw an axis-aligned rectangle.

Width and height are CSS pixels even when `top_left` is in chart space - keeps HUD-like badges the same size at any zoom level.

### Parameters

- `top_left` (`tuple`, default `required`): `(x, y)` for the top-left corner.
- `w_px` (`float`, default `required`): Width in CSS pixels.
- `h_px` (`float`, default `required`): Height in CSS pixels.
- `space` (`str`, default `"chart"`): `chart` or `pixel`.

### Returns

CanvasLayer (chainable)

### Example

```python
layer.style(fill="#08080bcc", stroke="#1f1f26")
layer.rect((10, 10), 120, 24, space="pixel")  # 120x24 px corner badge
layer.fill()
layer.style(fill="#f4f4f5", font="12px Inter")
layer.text((16, 26), "TF: tick", space="pixel")
```

<a id="canvaslayer-arc"></a>
## CanvasLayer.arc

```python
CanvasLayer.arc(center, radius_px, start_rad=0, end_rad=2*pi, *, space="chart")
```

Draw an arc / circle.

Radius is always CSS pixels. Defaults trace a full circle; pass partial angles for arcs.

### Parameters

- `center` (`tuple`, default `required`): `(x, y)` center point.
- `radius_px` (`float`, default `required`): Radius in CSS pixels.
- `start_rad` (`float`, default `0.0`): Start angle in radians.
- `end_rad` (`float`, default `2*pi`): End angle in radians.
- `space` (`str`, default `"chart"`): `chart` or `pixel`.

### Returns

CanvasLayer (chainable)

<a id="canvaslayer-text"></a>
## CanvasLayer.text

```python
CanvasLayer.text(point, s, *, space="chart", stroke=False)
```

Draw a text string.

Uses `font`, `text_align`, `text_baseline` from the current style. Set `stroke=True` to outline the text instead of filling.

### Parameters

- `point` (`tuple`, default `required`): Anchor point.
- `s` (`str`, default `required`): String to draw.
- `space` (`str`, default `"chart"`): `chart` or `pixel`.
- `stroke` (`bool`, default `False`): If True, outline-only (no fill).

### Returns

CanvasLayer (chainable)

<a id="canvaslayer-begin-close-stroke-fill"></a>
## CanvasLayer.begin / close / stroke / fill

```python
CanvasLayer.begin / close / stroke / fill()
```

Path-control ops mirroring the HTML5 canvas API.

`begin()` starts a new path; [`close()`](https://quantchartsllc.com/docs/python/py-data.md#close) connects the last point back to the first; `stroke()` paints the path with the current stroke style; `fill()` fills it. All chainable.

### Returns

CanvasLayer (chainable)

### Example

```python
# triangle entry marker
layer.style(fill="#22c55e88", stroke="#22c55e", line_width=1)
layer.begin()
layer.move_to((entry_ts, entry_price - 0.5))
layer.line_to((entry_ts + 60_000, entry_price))
layer.line_to((entry_ts, entry_price + 0.5))
layer.close()
layer.fill()
layer.stroke()
```

<a id="canvaslayer-emit"></a>
## CanvasLayer.emit

```python
CanvasLayer.emit()
```

Register the layer with the per-calculation buffer.

MUST be called at the end of building a layer or it gets dropped. Returns the spec dict (rarely useful) - the side effect is the registration.

### Returns

dict (the spec; usually ignored)

<a id="y_min-y_max-x_min-x_max"></a>
## Y_MIN / Y_MAX / X_MIN / X_MAX

Viewport-edge sentinels.

Pass these as the y or x component of a point to mean "viewport top/bottom edge" or "viewport left/right edge" without knowing the price scale or visible time range. The renderer resolves them at draw time, so they pan and zoom correctly.

Valid placements:
- `Y_MIN` / `Y_MAX` -> y component (chart-space points)
- `X_MIN` / `X_MAX` -> x component

Mixing axes (e.g. `Y_MIN` in the x slot) raises `ValueError`.

### Example

```python
from quant_charts import Y_MIN, Y_MAX
# vertical line that always spans the full visible price range
layer.move_to((ts, Y_MIN)).line_to((ts, Y_MAX)).stroke()
```
