Color functions

kScript (legacy) built colors at runtime: color.rgb, color.new, opacity, lighten, darken, blend, and a colorGradient that mapped a value onto a ramp per bar.…

kScript (legacy) built colors at runtime: color.rgb, color.new, opacity, lighten, darken, blend, and a colorGradient that mapped a value onto a ramp per bar. An Indicator never computes a color. Color is declared, once, on the output, box, segment, range, or renderer that carries it, and per-bar color is a ladder: a data-only output holds a palette index and the declaration names the palette. This page is what each color function becomes, and which have no form yet.

The reason is the same one behind every declaration: the chart restyles a package without recompiling it, every host draws the same thing, and the decision behind a color (a regime, a bucket) is a number a screen or an alert can read.

// Start from a base color, then knock it back to 40% opacity for a softer line.
output("sma20", line, overlay, { color: "#2563eb", opacity: 0.4, width: 2 });

What is available

kScriptIndicator
color.rgb(r, g, b)a literal: "rgb(37, 99, 235)", "#2563eb", or "hsl(221, 83%, 53%)"
color.new(color, transp), opacity(color, percent), transparency(...)the opacity option (0..1) on an output or box, or an 8-digit hex literal ("#2563eb66")
brightness, lighten, darken, blendnot in Indicators yet; write the resulting color as a literal
colorGradient(value, range, stops)a colors palette plus color_by over a bucketed output: a stepped gradient
palette("viridis")a colors literal array; there are no named palettes
a hex string or a named constant anywhere a color is expectedthe same strings; box fills need hex, rgb(), or hsl() because the fill carries the opacity (Color constants)

Runtime color math (lighten, darken, blend, a continuous gradient) waits on a color output channel; today color is declared and color_by is a bucket ladder.

Data-driven color with a ladder

colorGradient is the one to reach for when color should encode a value. The Indicator form: normalize the value into 0 .. n - 1, floor it, write it to a none output, and put an n-entry colors palette on the drawn output with color_by naming the index. Each bar's floored value indexes the palette; a missing or out-of-range value falls back to entry 0. The ramp is as fine as the palette is long, and the palette is yours: five stops of a viridis ramp below.

import { box, input, line, lower, none, ohlcv, output, overlay, param } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_band_hi, out_band_lo, out_heat, out_rsi, out_sma20, out_trend, out_trend_up } from "./gen/outputs";
import { p_period } from "./gen/params";
import { Rsi, Sma } from "./sdk/ta";

param("period", 14, { min: 2, max: 200 });
input("close", ohlcv.close);
// colorGradient(rsi, [0, 100], palette("viridis")) as a five-step ladder: cool when oversold, hot when overbought.
output("rsi", line, lower, { width: 2, color_by: "heat", colors: ["#440154", "#3b528b", "#21918c", "#5ec962", "#fde725"], description: "RSI colored by its own level" });
output("heat", none, lower, { description: "0..4: which fifth of 0..100 the RSI sits in" });
// opacity(color, 40) as a declared opacity.
output("sma20", line, overlay, { color: "#2563eb", opacity: 0.4, width: 2, description: "A faded average" });
// blend(green, red, weight) per regime as two literals and a 0/1 index.
output("trend", line, overlay, { width: 2, color_by: "trend_up", colors: ["#dc2626", "#16a34a"], description: "The average, red falling, green rising" });
output("trend_up", none, overlay, { description: "1 while the average rises" });
// An 8-digit hex fill: the last two digits are the alpha.
const bandHi = output("band_hi", none, overlay);
const bandLo = output("band_lo", none, overlay);
box("band", { top: bandHi, bottom: bandLo, color: "#2563eb", opacity: 0.12, borderWidth: 0 });

let rsi = new Rsi(14);
let sma = new Sma(20);
let strength: f64 = NaN;
let average: f64 = NaN;
let prevAverage: f64 = NaN;

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

export function state(): i32 {
  const close = in_close();
  strength = rsi.update(close);
  prevAverage = average;
  average = sma.update(close);
  return isNaN(strength) || isNaN(average) ? 0 : 1;
}

export function finalize(): void {
  // Bucket 0..100 into five steps; 100 itself lands in the last bucket.
  let bucket = Math.floor(strength / 20.0);
  if (bucket > 4.0) bucket = 4.0;
  out_rsi(strength);
  out_heat(bucket);
  out_sma20(average);
  out_trend(average);
  out_trend_up(!isNaN(prevAverage) && average >= prevAverage ? 1.0 : 0.0);
  out_band_hi(average * 1.01);
  out_band_lo(average * 0.99);
  emitRow();
}

export function reset(): void {
  rsi.reset();
  sma.reset();
  strength = NaN;
  average = NaN;
  prevAverage = NaN;
}

The heat output is a metric: om metric series on it returns the bucket per bar, which is the kind of value a screen ranks by.

Tinting bars by value

The same ladder drives a background tint through render.bgcolor, the port of plotBgColor and barcolor: a where gate says which bars get a tint, and color_by with colors picks it per bar. A non-finite index means no tint on that bar (the static color never substitutes), so a NaN bucket is a clean way to leave a bar alone.

import { input, line, lower, none, ohlcv, output, param, render } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_extreme, out_rsi, out_zone } from "./gen/outputs";
import { p_overbought, p_oversold, p_period } from "./gen/params";
import { Rsi } from "./sdk/ta";

param("period", 14, { min: 2, max: 100, description: "RSI Period" });
param("overbought", 70, { min: 50, max: 100, description: "Overbought Level" });
param("oversold", 30, { min: 0, max: 50, description: "Oversold Level" });
input("close", ohlcv.close);
output("rsi", line, lower, { color: "#2962ff", width: 2, description: "Relative strength index" });
output("zone", none, lower, { description: "0 oversold, 1 overbought: the tint ladder index; NaN in between" });
output("extreme", none, lower, { description: "1 on bars in either zone: the tint gate" });
// plotBgColor(...) per condition: one declaration, a gate, and a two-entry ladder.
render.bgcolor("zones", { where: "extreme", color_by: "zone", colors: ["rgba(76, 175, 80, 0.3)", "rgba(244, 67, 54, 0.3)"] });

let rsi = new Rsi(14);
let overbought: f64 = 70.0;
let oversold: f64 = 30.0;
let value: f64 = NaN;

export function init(): void {
  rsi = new Rsi(i32(p_period()));
  overbought = p_overbought();
  oversold = p_oversold();
}

export function state(): i32 {
  value = rsi.update(in_close());
  return isNaN(value) ? 0 : 1;
}

export function finalize(): void {
  const hot = value > overbought;
  const cold = value < oversold;
  out_rsi(value);
  out_zone(cold ? 0.0 : hot ? 1.0 : NaN);
  out_extreme(hot || cold ? 1.0 : 0.0);
  emitRow();
}

export function reset(): void {
  rsi.reset();
  value = NaN;
}

kScript's five nested plotBgColor branches (extreme oversold, oversold, neutral, overbought, extreme overbought) are a five-entry palette and a bucket that lands in 0 .. 4; the module above keeps two for clarity.

Transforms that have no form

brightness, lighten, darken, and blend mixed a color at runtime. Not in Indicators yet. Compute the color you want ahead of time and write it as a literal; a blend(a, b, 0.5) that depended on a per-bar weight becomes a ladder over a few blended literals, and blend.amount becomes the bucket. There is no color type in the module: a param cannot hold a color, a string slot could carry one but no renderer reads a color from a slot, and every colors entry is a string literal the extractor reads without running the code.