---
title: Drawing Primitives
description: >-
  Per-bar visual primitives: the plotShape marker set with locations and
  character markers, barcolor(), fillBetween(), and rich table cells.
---

<div class="flex gap-3 mb-6">
  <span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-blue-50 text-blue-600 text-sm font-medium">
    Intermediate
  </span>
  <span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-gray-100 text-gray-600 text-sm font-medium">
    8 min read
  </span>
</div>

## Introduction

These are the **declarative per-bar** visuals (the counterpart to the stateful [Drawing Objects](drawing-objects.md)): markers, candle recoloring, and band fills emitted bar by bar like any plot.

## Shape markers

`plotShape` supports ten shapes:

`circle`, `cross`, `triangle`, `diamond`, `arrowUp`, `arrowDown`, `flag`, `square`, `label`, `char`

```javascript
if (crossover(fast, slow)) {
  plotShape(low, shape="arrowUp", location="belowBar", colorIndex="#16a34a")
}
```
### Locations

The `location` parameter places the marker relative to the bar:

| Value | Placement |
|---|---|
| `"aboveBar"` | Above the bar's high |
| `"belowBar"` | Below the bar's low |
| `"top"` / `"bottom"` | Pinned to the pane edge |
| `"absolute"` | At the supplied y value (the default; legacy behavior) |

### Character markers

```javascript
plotShape(high, shape="char", char="B", location="aboveBar")


`char` must be exactly one character; anything else is a line/column error. Invalid shapes and locations are caught at compile time when literal, at runtime otherwise, always with position info.
```
## barcolor

Recolor the bar's candle itself:

```javascript
timeseries delta = fp.cells.map((c) => c[2] - c[3]).reduce((s, x) => s + x, 0)

if (delta > 0) { barcolor("#16a34a") }
if (delta < 0) { barcolor("#dc2626") }
```



Bars you do not color keep the chart default. Colors go through the standard [color validation](../core-concepts/na-and-scalar-types.md) (typed colors and strings both work). Delta-colored candles, driven by real per-bar buy/sell volume, are a one-screen kScript example with no Pine equivalent.
## fillBetween

Fill the band between two plotted series:

```javascript
timeseries upper = sma(source=trade_data, period=20) + stdev(source=trade_data, period=20) * 2
timeseries lower = sma(source=trade_data, period=20) - stdev(source=trade_data, period=20) * 2

fillBetween(upper, lower, "#0ea5e9", 0.15)


`fillBetween(series1, series2, color, opacity?)` references the two series rather than copying them. Both arguments must be plottable series (line/column error otherwise). Conditional fills are fine: bars where you do not call it simply have no band.
```
## Rich table cells

`plotTable` (the declarative dashboard plot, distinct from the `table.new` drawing object) accepts **per-cell objects** alongside plain scalars:

```javascript
plotTable([
  ["Metric", "Value"],
  [{ value: "CVD", tooltip: "cumulative volume delta" },
   { value: tostring(cvd, "0.00"), color: cvd > 0 ? "#16a34a" : "#dc2626" }]
], position="top_right")


Cell fields: `value`, `color`, `textColor`, `tooltip`, `align`, `colspan`, `rowspan`. Scalar cells behave exactly as before (full backward compatibility); spans must be positive integers; colors are validated; misuse reports line/column.
```
## Everything rolls back correctly

All of these primitives are per-bar outputs, so they participate in the engine's forming-bar rollback: a conditional `barcolor` or `fillBetween` evaluated during a live, still-forming bar is replayed cleanly on every tick. The differential harness pins this byte-for-byte.
