---
title: "NaN and color"
description: "Working with missing values in an Indicator: NaN and the two-line helpers that replace the isna, nz, and fixnan builtins of kScript (legacy), how NaN behaves…"
order: 18
section: "core-concepts"
---

<!-- source: docs/indicators/core-concepts/na-and-scalar-types.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# NaN and color

Working with missing values in an Indicator: `NaN` and the two-line
helpers that replace the `isna`, `nz`, and `fixnan` builtins of
kScript (legacy), how `NaN` behaves in arithmetic and on the chart, and how
color works when it is a declaration rather than a value. kScript had `na` as a
value and `color` as a first-class type with constructors; an Indicator has `NaN`
inside `f64` and colors as string literals on outputs, boxes, and segments.

## Why NaN matters

`NaN` is an Indicator's "no value here." You meet it constantly: a TA
class has not warmed up yet, a sparse source has no observation on this
bar, a ratio has a zero denominator. The important rule is that **`NaN` is
contagious in arithmetic**: any expression touching a `NaN` becomes `NaN`.
Add `5` to a not-yet-warm SMA and you get `NaN`, and a `NaN` written to an
output draws as a gap, not a zero. So the skill is detecting `NaN` and
deciding what to do about it before it poisons a calculation or blanks a
line.

The helpers, and what they are in an Indicator:

| kScript | Indicator | Use it to |
| --- | --- | --- |
| `isna(x)`, `isnan(x)` | `isNaN(x)` | branch on whether a value is missing |
| `isnum(x)` | `isFinite(x)` (`!isNaN(x)` when infinities are impossible) | the inverse: is this a usable number |
| `nz(x, fallback)` | `isNaN(x) ? fallback : x` | replace a missing value with a fallback |
| `fixnan(series)` | a module-level `held` updated only when the new value is finite | hold the last good value forward over gaps |
| `NaN == na` | `isNaN(x)`; never `x == NaN` | `NaN` is not equal to anything, itself included |

Comparisons with `NaN` are always false: `NaN > 0.0`, `NaN < 0.0`, and
`NaN == NaN` all evaluate `false`. That is why a condition built over a
warming input never fires by accident, and why `isNaN` is the only test
that works.

## Filling and forward-holding

`nz` is the everyday tool and it is one ternary: if `x` is `NaN`, take the
fallback, otherwise take `x`. It is how you keep a plot continuous or keep
arithmetic finite.

`fixnan` is the series-level cousin: wherever the series is `NaN`,
substitute the most recent finite value, so a gappy series becomes a
stepped, continuous one. In an Indicator that is a module-level variable
you overwrite only when the new value is finite, and `reset()` clears it.

Both, plus the two ways a row can be missing, in one file. The `propagated`
output is deliberately `NaN` for the first 20 bars: the SMA has not warmed
up, and adding `5` to `NaN` stays `NaN`. `filled` patches those bars with
the close, and `held` carries the last finite liquidation reading across
quiet bars (`missing: "nan"` keeps the source honest; the holding is the
module's decision).

```typescript
import { box, input, line, liquidations, lower, ohlcv, output, overlay, param } from "./sdk/declare";
import { in_close, in_liqs } from "./gen/inputs";
import { emitRow, out_band_hi, out_band_lo, out_filled, out_held, out_propagated } from "./gen/outputs";
import { p_period } from "./gen/params";
import { Sma } from "./sdk/ta";

param("period", 20, { min: 1, max: 200, description: "SMA length" });
input("close", ohlcv.close);
input("liqs", liquidations.liquidations, { missing: "nan", description: "Liquidation volume, NaN on bars without any" });
output("propagated", line, overlay, { color: "#94a3b8", description: "SMA + 5: NaN until the window is warm, and NaN stays NaN" });
output("filled", line, overlay, { color: "rgb(37, 99, 235)", width: 2, description: "The same series with the close as a fallback while warming (nz)" });
output("held", line, lower, { color: "#7c3aed", description: "The last finite liquidation reading, held across quiet bars (fixnan)" });
const bandHi = output("band_hi", line, overlay, { color: "#2563eb66", description: "Filled series plus 1%" });
const bandLo = output("band_lo", line, overlay, { color: "#2563eb66", description: "Filled series minus 1%" });
// A box fill takes the opacity, so its color is hex, rgb(), or hsl(): a named color is refused here.
box("band", { top: bandHi, bottom: bandLo, color: "hsl(221, 83%, 53%)", opacity: 0.1, borderWidth: 0 });

let sma = new Sma(20);
let close: f64 = NaN;
let propagated: f64 = NaN;
let held: f64 = NaN; // the fixnan state: overwritten only by a finite value

function nz(x: f64, fallback: f64): f64 {
  return isNaN(x) ? fallback : x;
}

export function init(): void {
  sma = new Sma(i32(p_period()));
}

export function state(): i32 {
  close = in_close();
  propagated = sma.update(close) + 5.0; // NaN + 5 is NaN: the contagion
  const liqs = in_liqs();
  if (isFinite(liqs)) held = liqs; // fixnan: hold the last good value forward
  return 1;
}

export function finalize(): void {
  const filled = nz(propagated, close);
  out_propagated(propagated);
  out_filled(filled);
  out_held(held);
  out_band_hi(filled * 1.01);
  out_band_lo(filled * 0.99);
  emitRow();
}

export function reset(): void {
  sma.reset();
  close = NaN;
  propagated = NaN;
  held = NaN;
}
```

Walking the key lines:

- `propagated = sma.update(close) + 5.0` is `NaN` for the first 20 bars.
  Written to its output, those bars are a gap on the grey line; the row
  itself is still ready (`state()` returns `1`), so the other outputs draw.
- `nz(propagated, close)` patches the early bars with the close, so the
  blue line is continuous from bar 0 instead of starting blank at bar 20.
- `if (isFinite(liqs)) held = liqs;` is `fixnan`: a bar with no
  liquidations reads `NaN` under `missing: "nan"`, and the held value
  steps only when a real reading arrives. Before the first reading `held`
  is `NaN`, an honest gap rather than an invented zero.

What you'll see: a grey line that starts at bar 20 and a blue one that
starts at bar 0, a faint band around the blue one, and a stepped purple
line in the lower pane.

### Abstaining is the other option

Writing `NaN` to one output leaves the row ready and the other outputs
drawing. Returning `0` from `state()` abstains the whole row: nothing is
drawn, no metric value exists for that bar, and `finalize()` is not
called. Use `NaN` when one line warms slower than another; abstain when
nothing on the row is meaningful yet (`execution-model.md`).

## Colors are declared, not built

kScript built colors at runtime with `color.rgb(r, g, b)` and
`color.new(color, transp)` and stored them in variables. An Indicator has
no color type and no color values: a color is a string literal in a
declaration, read once when the sheet is derived. The forms the chart
understands:

| Form | Example | Where |
| --- | --- | --- |
| Six-digit hex | `"#2563eb"` | everywhere |
| Eight-digit hex (alpha in the last two digits) | `"#2563eb66"` | everywhere; the built-in way to say `color.new(c, transp)` |
| `rgb()` / `rgba()` | `"rgb(37, 99, 235)"` | everywhere |
| `hsl()` / `hsla()` | `"hsl(221, 83%, 53%)"` | everywhere |
| A named color | `"orange"` | outputs, segments, renderers, borders; refused on a box fill |

Alpha has two spellings: an eight-digit hex (or `rgba()` / `hsla()`) bakes
it into the color, and the `opacity` option (0..1) on an output, a box, or
a range applies it on top. A box's fill takes its opacity, which is why a
box `color` must be a form the host can rewrite with an alpha channel:
hex, `rgb()`, or `hsl()`. A named color there is refused at validation
with `color must be a hex, rgb() or hsl() color (the fill takes the
opacity)`. The named palette, and where each form is accepted, is
`color-constants.md`.

A color that must change per bar is not a value either: it is a data-only
output indexing a declared `colors` palette through `color_by`, floored,
with a missing or out-of-range value falling back to entry 0
(`functions/color-functions.md`).

## Channel values are validated

The sheet validator checks a box fill's form before the module runs, and
the chart parses every other color string when it draws. A channel outside
its range (`rgb(300, 0, 0)`) is not a build error, but it is not a color
the chart can render either, so keep channels in `0`..`255` and hue,
saturation, and lightness in their own ranges. The safe habit is six- or
eight-digit hex everywhere: it validates on a box fill, it carries alpha,
and it is what every worked example in this tree uses.
