Computed — derived values
Use
computed name = expressionto declare a derived variable — auto-recomputes when source variables change. Pull "complex expressions" out of templates so interact blocks stay readable, composable, and chain-derivable.
When to use
- ✅ Same expression referenced multiple times in template (avoid duplication)
- ✅ Multi-step computation (intermediate values are meaningful and worth naming)
- ✅ Conditional categorization (use ternary to map numbers to labels)
- ✅ Reuse across charts / templates (multiple blocks share one derivation)
- ❌ Simple one-off expressions (write directly in template, more direct)
- ❌ Complex business logic / side effects / async (the mini DSL is pure-function — no IO / state mutation)
Basic syntax
```interact
slider weight range=40..120 step=1 default=70 label="Weight (kg)"
slider height range=1.4..2.1 step=0.01 default=1.7 label="Height (m)"
computed bmi = weight / (height * height)
template stl:
[BMI] -> [{round(bmi, 1)}]
```
computed <name> = <expression> declares a derived variable. Expressions evaluate in declaration order.
Renders: drag any slider → bmi recomputes → template updates live.
Expression mini-DSL — full spec
Arithmetic
| Operator | Syntax | Example |
|---|---|---|
| Add / sub / mul / div | + - * / |
a + b * c |
| Modulo | % |
n % 10 |
| Power | ** or pow(a,b) |
2 ** n or pow(2, n) |
Comparison + logical
| Operator | Syntax | Example |
|---|---|---|
| Comparison | == != < > <= >= |
age >= 18 |
| Logical | && || ! |
enabled && !muted |
| Ternary | cond ? a : b |
bmi < 18.5 ? 'thin' : 'normal' |
Built-in functions
38 built-in functions, in the order the reference implementation lists them (src/utils/exprDSL.ts):
| # | Function | Description |
|---|---|---|
| 1 | abs(x) |
Absolute value |
| 2 | sqrt(x) |
Square root |
| 3 | cbrt(x) |
Cube root |
| 4 | pow(x, y) |
Power (same as x ** y) |
| 5 | exp(x) |
e^x |
| 6 | ln(x) |
Natural logarithm (base e) |
| 7 | log(x[, base]) |
Logarithm; default base 10, or the given base |
| 8 | log2(x) |
Base-2 logarithm |
| 9 | log10(x) |
Base-10 logarithm |
| 10 | sin(x) |
Sine (radians) |
| 11 | cos(x) |
Cosine |
| 12 | tan(x) |
Tangent |
| 13 | asin(x) |
Arcsine (returns radians) |
| 14 | acos(x) |
Arccosine |
| 15 | atan(x) |
Arctangent |
| 16 | atan2(y, x) |
Two-argument arctangent |
| 17 | round(x[, d]) |
Round half-up, optional decimal places |
| 18 | floor(x) |
Round toward -∞ |
| 19 | ceil(x) |
Round toward +∞ |
| 20 | trunc(x) |
Round toward zero |
| 21 | sign(x) |
Sign: -1, 0, or 1 |
| 22 | min(a, b, ...) |
Minimum of arguments |
| 23 | max(a, b, ...) |
Maximum of arguments |
| 24 | sum(a, b, ...) |
Sum of arguments |
| 25 | avg(a, b, ...) |
Mean of arguments |
| 26 | clamp(x, lo, hi) |
Clamp x into [lo, hi] |
| 27 | mod(x, n) |
Modulo (always-positive remainder) |
| 28 | lerp(a, b, t) |
Linear interpolation: a + (b - a) * t |
| 29 | mix(a, b, t) |
Alias for lerp |
| 30 | smoothstep(a, b, x) |
Smooth Hermite step: 0 below a, 1 above b |
| 31 | deg(r) |
Radians → degrees |
| 32 | rad(d) |
Degrees → radians |
| 33 | if(cond, a, b) |
Conditional (alternative to cond ? a : b) |
| 34 | select(x, [v0, v1, ...]) |
Pick the element at index round(x) from an array literal (out of range → nan) |
| 35 | concat(a, b, ...) |
Concatenate arguments as a string |
| 36 | len(s) |
Length of the string form |
| 37 | upper(s) |
Uppercase |
| 38 | lower(s) |
Lowercase |
Constants
| Name | Value |
|---|---|
pi |
3.14159... |
e |
2.71828... |
tau |
2π |
inf |
Infinity |
nan |
NaN |
Not supported (safety design)
- ❌
eval/Functionconstructor - ❌ DOM / window / document access
- ❌
fetch/ network - ❌
setTimeout/ async - ❌
if/elsestatements (use theif(...)function or ternary) - ❌ for/while loops (use vega-lite transforms or svg multi-element generation)
- ❌ Function definitions / closures (no
function foo() {...}) - ❌ Attribute access (
obj.field) / subscript indexing (arr[i]) — array literals are allowed, but only as the second argument toselect(...) - ❌ Explicit string↔number casting helpers, and any kind of sub-string/slice-by-index helper — not on the whitelist. Numeric-looking
input/select/togglevalues are auto-coerced when used in arithmetic; the only string-shape operations available areconcat/len/upper/lower(see the function table above) — there's no way to pull out part of a string
Design goal: pure-function expressions, absolutely safe (the reader needs no sandbox), easy for LLMs to generate without errors.
Examples
Example 1: BMI calculation + classification
```interact
slider weight range=40..120 step=1 default=70 label="Weight (kg)"
slider height range=1.4..2.1 step=0.01 default=1.7 label="Height (m)"
computed bmi = weight / (height * height)
computed category = bmi < 18.5 ? "Underweight"
: bmi < 24 ? "Normal"
: bmi < 28 ? "Overweight"
: "Obese"
template stl:
[BMI] -> [{round(bmi, 1)}] ::mod(category="{category}")
```
Renders: BMI number + auto-classified label. category uses nested ternaries to map number to label.
Example 2: compound interest — multi-step chained derivation
```interact
slider principal range=1000..100000 step=1000 default=10000 label="Principal"
slider rate range=0..0.15 step=0.01 default=0.05 label="Annual rate"
slider years range=1..30 step=1 default=10 label="Years"
computed multiplier = pow(1+rate, years)
computed compound = principal * multiplier
computed gain = compound - principal
computed gainPercent = (gain / principal) * 100
template stl:
[Principal] -> [${round(principal, 0)}]
[Multiplier] -> [{round(multiplier, 3)}x]
[Final value] -> [${round(compound, 0)}]
[Net gain] -> [${round(gain, 0)} ({round(gainPercent, 1)}%)]
```
Renders: 4 output lines. multiplier → compound → gain → gainPercent chained derivation; each intermediate value referenced in template.
Example 3: physics — spring oscillator position
```interact
slider amplitude range=0..5 step=0.1 default=2 label="Amplitude"
slider frequency range=0.1..5 step=0.1 default=1 label="Frequency"
slider phase range=0..6.28 step=0.1 default=0 label="Phase"
slider t range=0..10 step=0.1 default=0 label="Time"
computed angularFreq = 2 * pi * frequency
computed position = amplitude * sin(angularFreq * t + phase)
computed velocity = amplitude * angularFreq * cos(angularFreq * t + phase)
template stl:
[Position x(t)] -> [{round(position, 3)} m]
[Velocity v(t)] -> [{round(velocity, 3)} m/s]
```
Renders: 4 sliders control spring parameters; 2 lines output position + velocity (based on SHM physics formula). angularFreq = 2πf is the intermediate derivation.
Example 4: toggle-driven conditional branches
```interact
slider price range=0..1000 step=1 default=100 label="Price"
toggle isPremium default=false label="Premium member"
toggle hasCoupon default=false label="Has coupon"
computed memberDiscount = isPremium == 'true' ? 0.2 : 0
computed couponDiscount = hasCoupon == 'true' ? 0.1 : 0
computed totalDiscount = min(memberDiscount + couponDiscount, 0.3)
computed finalPrice = price * (1 - totalDiscount)
template stl:
[Original] -> [${price}]
[Member off] -> [{round(memberDiscount * 100, 0)}%]
[Coupon off] -> [{round(couponDiscount * 100, 0)}%]
[Total off] -> [{round(totalDiscount * 100, 0)}% (≤30% cap)]
[Final price] -> [${round(finalPrice, 2)}]
```
Renders: toggles switch member / coupon; toggle values are the strings "true"/"false" so each is compared with == 'true'; totalDiscount uses min(...) to apply 30% cap. Logic stays in declarations, not imperative.
Plain-text fallback behavior
computed declarations are fully visible in plain-text readers:
```interact
slider weight range=40..120 step=1 default=70 label="Weight (kg)"
slider height range=1.4..2.1 step=0.01 default=1.7 label="Height (m)"
computed bmi = weight / (height * height)
template stl:
[BMI] -> [{round(bmi, 1)}]
```
→ Readers see computed bmi = weight / (height * height) and fully understand the derivation. Plain text doesn't evaluate, but the expression itself is well-documented.
Common pitfalls
1. Circular dependency
```interact
computed a = b + 1 ← ❌ Mutual dependency
computed b = a + 1
template stl:
[a] -> [{a}]
```
→ Rho's topological sort fails. Computeds can't form a cycle — there is no click-to-increment control and no "previous value + 1" accumulator pattern in the spec at all. For step-by-step / accumulating state, drive the sequence off a timer instead — see Timer.
2. computed references undeclared variable
```interact
slider x range=0..10 step=1 default=5 label="x"
computed y = x + z ← ❌ z not declared
template stl:
[y] -> [{y}]
```
→ Expression throws "undefined z". Declare all variables (slider/input/computed) first.
3. Expression syntax errors
computed bmi = weight / height ** 2 ← ⚠️ Precedence confusion; actually weight / (height ** 2)
computed bmi = (weight / height) ** 2 ← ❌ Wrong formula
computed bmi = weight / (height * height) ← ✅ Recommended: explicit parens
computed bmi = weight / pow(height, 2) ← ✅ Recommended: use pow function
Use parentheses to express precedence explicitly — don't rely on implicit precedence; avoids reader (and LLM) miscomputation.
4. Ternary nesting too deep
computed grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : score >= 60 ? "D" : "F"
3-level ternary is near readability limit. For 4+ levels, use multi-line format:
computed grade = score >= 90 ? "A"
: score >= 80 ? "B"
: score >= 70 ? "C"
: score >= 60 ? "D"
: "F"
Or split into multiple computeds:
computed isHigh = score >= 90
computed isMid = score >= 70 && score < 90
computed grade = isHigh ? "A" : isMid ? "C" : "F"
5. Expression with string operations
input city default="Shanghai" label="City"
computed greeting = "Hello, " + city + "!" ← ❌ `+` doesn't concatenate strings
computed greeting = concat("Hello, ", city, "!") ← ✅ use the concat() function
The mini DSL's + is arithmetic-only. String concatenation goes through concat(...) — or, for a simple one-off, interpolate directly in the template: Hello, {city}!.
6. Floating-point precision loss
computed total = 0.1 + 0.2 ← May produce 0.30000000000000004
template stl:
[Result] -> [{total}]
→ Floats are IEEE 754 — finite precision, though the renderer's default number formatting already rounds to ~6 significant digits. For money / ratios, force a specific precision with round(expr, N): {round(total, 2)} → 0.3.
7. Too many computeds
```interact
slider a range=0..10 step=1 default=5 label="a"
computed b = a * 2
computed c = b + 1
computed d = c / 3
computed e = pow(d, 2)
computed f = sqrt(e)
... (15 computeds)
```
→ Maintenance nightmare. Redesign: combine multiple computeds into one expression (if intermediates aren't referenced); or split across multiple interact blocks (sharing via namespace).
Computed vs template expression — choose
| Scenario | Use |
|---|---|
| One-off simple expression | Write {a + b} directly in template |
| Same expression in template ≥ 2 times | Extract as computed |
| Multi-step derivation (intermediates have meaningful names) | Multiple computeds chained |
| Reused across templates / charts | computed |
See also
- Interact controls — slider / input / select / toggle
- Interact text template — template syntax
- Interact + Vega-Lite chart — charts can reference computeds too
- Timer — auto-play (also drives computeds)
- Shared state (namespace)
- Plain-text fallback principle