Computed — derived values

Use computed name = expression to 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
inf Infinity
nan NaN

Not supported (safety design)

  • eval / Function constructor
  • ❌ DOM / window / document access
  • fetch / network
  • setTimeout / async
  • if/else statements (use the if(...) 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 to select(...)
  • ❌ Explicit string↔number casting helpers, and any kind of sub-string/slice-by-index helper — not on the whitelist. Numeric-looking input/select/toggle values are auto-coerced when used in arithmetic; the only string-shape operations available are concat / 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. multipliercompoundgaingainPercent 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

Open in Rho MD →