← Interactive tools

The making of the Random Variable Visualizer

A curated transcript of the conversations that built the Random Variable Visualizer.

A curated transcript of the conversations (with Claude Code) that produced the Random Variable Visualizer for tao-web — an interactive realization of the 2016 blog post "Visualising random variables" — from the first sketch of the language through events, "universes", and a stabilized syntax.

How to read this. As with the spacetime and Gilbreath transcripts, the aim is to record the process:

The app was built over several sessions: the core was prototyped in one, then moved into tao-web and extended in others. The engine was checked with headless Node unit tests throughout (growing from a few dozen to ~160 checks), and the UI wiring with a DOM-shim smoke test. Nothing was pushed until each stage had been tested locally over file://.


1. The idea — random variables as animated quantities

Terence (opening the original session):

I'm interested in developing another app, eventually to upstream to my tao-web site. The basic idea is to implement the ideas I laid out in "Visualising random variables" where at the time I had to use animated GIFs to illustrate my idea of visualizing random variables as animated quantities. I'm thinking of having this app implement a very basic programming language where one can have assignments such as X := Unif({1,..,6}), which makes X a random variable uniformly drawn from {1,...,6} (and independent of any previous random variables), Y := Exp(3) […] Z := X+Y which makes Z the sum of X and Y (and thus coupled to X and Y in a complicated fashion) […]. On some other panel, all the values of the random variables are displayed (X = 2, Y = 1.56, Z = 3.56, …), refreshed at a given timestep. Then, on a third panel, one has the ability to plot either a single random variable (as a sequence of dots on a real line, updating in time, with previous instantiations given some fading color), or a pair of random variables (in a two-dimensional space). Can you read through the blog post and help workshop this app idea?

Claude read the post and proposed the architecture the app still uses: variables are stored as an expression graph over primitive draws, not as numbers; every tick a single shared sample point ω is drawn and all primitives resample together, so coupling (Z = X + Y stays tied to X and Y) and independence (two separate draws) hold exactly. It proposed a small Excel-like language, a three-panel layout (program / live values / animated Monte-Carlo plot), and a DOM-free core that could be unit-tested in Node.

2. Scope — one shared ω, conditioning, and an early note on "universes"

Terence (settling the model; this message also raised the "universes" idea built much later as (X | E)):

Refresh should be global. For the initial version we should go with display filtering, to keep a single underlying source of randomness, but we could have a setting where the rejection sampling is done near-instantly […] (though there should be some limiter to stop the app consuming all the CPU cycles when trying to condition on a <0.001 probability event). In later versions I could imagine having multiple probability spaces […]; doing something like Y := (X | X > 3) would place Y in a different "probabilistic universe" than X where it is driven by a different random source omega; it no longer becomes possible to plot X against Y directly. But this can be deferred to a later stage.

I like the idea of starting with a relatively narrowly scoped language but with an eye of building up to more features later on. A fully Turing complete language may be too much for an educational app […]. Each line in this language should be self-explanatory and not intimidating to an undergraduate probability student; this would be for a math class, not a programming class […] (my mental model is something similar to the Excel expression language). One useful command would be condition on E, which is like a probabilistic "assert", changing the underlying probability distribution so that one rejects any state for which E fails.

This message fixed several of the design choices the project kept to: readability over expressive power (explicitly not Turing-complete), an Excel-like feel, condition on E as rejection sampling (with a per-tick try cap for rare events), and the (X | E) "universe" idea, deferred at the time. Claude built the first prototype: the tokenizer/parser, the shared-ω evaluator, the standard distributions, condition on, and the animated 1-D/2-D plots.

Terence added first-class constants (parameters and vector sizes, distinct from random variables) and floated matrices/EIGENVALUES as a possible later addition — a judgment revisited near the end of the project.

Terence (early polish):

Some edits: (a) I've switched from Commonwealth to US spelling […]. (b) […] I'd also like to see an option where the color varies more continuously from the current color to the previous colors […]. (c) […] I'd like the active dot to be subject to the same fuzzing as the past trail; one can have an option to set how much fuzzing there is, or disable it entirely. […] Theoretical curve overlay (or heat map, in the 2D case) would be great, but […] perhaps one can fail gracefully […] give up if not, in which case it is not displayed (with some unobtrusive error message that the theoretical curve is unavailable). Shareable scenarios sound great, maybe hashed in some way.

Claude added adjustable fuzz (perpendicular-only, so the value stays exact), a continuous colour-gradient trail, a theory overlay that draws the exact pdf/pmf when the variable is a single primitive distribution and reads "unavailable" otherwise, US spelling, and share links (the program base64url-encoded in the URL). The blog post and its commenters were credited in the footer.


3. Bringing it into tao-web

The app had been prototyped in a standalone folder. A separate session, running inside the tao-web repo, was asked to adopt it.

Terence:

I started an external Claude session to develop a new probability visualizer app. I have since asked that session to move the app and its production notes into tao-web. Can you locate this app, and are you able to understand the current state of development?

Claude located the relocated random-variables.html / random-variables.js, read the production notes, and skimmed the original session's transcript for the plan. The move had deleted the original test suite; told to reconstruct it independently, Claude rebuilt the headless engine test from scratch (distribution means, exact coupling, conditioning).

Terence:

I don't want to push to the public repo yet and am happy to test a local page first until the features stabilize, but I'd like to get to the point where there is a page using the css of tao-web.

Claude moved the app onto the site's applet.css (light/dark aware), deriving the canvas palette from CSS tokens each frame so the plot follows the theme, and reworked the page into the standard scrollable document shell.

4. A histogram bug, and multiple plot windows

Terence:

Also there is something a bit odd I noticed when comparing the empirical histogram of Unif({1,..,6}) against the theoretical distribution; the empirical distribution always undershoots, which should be impossible because both distributions have to sum to 1.

This was a normalization bug: the histogram was scaled to its own peak while the theory pmf was scaled to its peak, so a flat pmf (a fair die) always undershot the equal-height theoretical stems. Claude changed both to a shared vertical scale, and also set the dice preset to 1:1 aspect with reduced fuzz and darkened the theory curve for contrast.

Terence asked for multiple plot windows (let's do the multiplot windows), then reported a rendering issue:

I guess the three coupled views work better when the 1:1 flag is turned off, so that the X variables align. It stretches out the fuzz by doing so, due to the nearly 3:1 aspect ratio; can one make the fuzz isotropic regardless of aspect ratio to look nicer?

Claude reworked plotting around a per-window model (each plot line opens its own window with its own trail and bounds) and made 2-D fuzz isotropic — a pixel radius added after projection, so the jitter cloud stays round in a non-square window. A DOM-shim boot test was added to drive the real inline script headlessly.

5. Publishing a pre-alpha (and a documentation page)

Terence:

There is already interest in this app on my blog, so I'd like to push out a pre-alpha version on the repository. First I'd like to add some header text on top of the probability app similar to what is done for other apps such as the relativity app. Also I'd like to link it to the blog post […] which I would also like to add as a "short story" to tao-web. […] the nascent language we have built into the app requires its own separate documentation page with complete lists of functions etc.; the short "cheat sheet" built into the app is helpful but incomplete. Perhaps the best place to put that documentation is at the bottom of the applet page, perhaps with dropdowns to expand for function lists.

Claude wrote the intro header (with a pre-alpha badge and a work-in-progress callout), added a collapsible language reference at the foot of the page, catalogued the app under a new Probability category with a new in-development status (a purple pill in the applet gallery), and added the 2016 post to the works database as a short story linking back to the app. First push, first green deploy.


6. Empirical statistics — a new class of object

Terence:

I think I'm ready to return to developing the random variable visualizer. One thing I think is that the speed bar should be more discrete and log-scale. There's not much point in having both speed 267 and speed 268 […]. On the other hand, having very slow speeds like 0.5 or 0.25 would be good. […] In fact just the powers of 2 may be enough.

Claude made the speed slider a power-of-2 log scale (and, later, the trail slider too, for the same reason). Then the more substantial request:

Terence:

I like the existing functionality of having the live variables autotrack their empirical mean and standard deviation. One could add a code feature to track other empirical observables, such as variance, covariance, and correlation. So for instance track Cov(X, Y) would add a tracker to the values pane […]. Note that covariance, variance, etc. are not functions in the existing sense of turning random variables to more random variables; they are instead statistics that turn a time series of random variables into a number.

This introduced a separate kind of object. Claude added the statistic / track command with exact online moments (Mean, Var, SD, Cov, Corr) and sample-based statistics over a bounded ring buffer (Median, quartiles, Percentile, Mode, Shannon Entropy, running Max/Min), kept in their own namespace and usable only inside the command. In the same exchange Terence asked to forbid reassignment — X = X + 1 should be illegal — to keep students from treating a random variable as a mutable cell; Claude made a variable immutable once defined.

7. Editor features — highlighting and hovers from one glossary

Terence:

Ooh, can one implement text coloring in the programming section to color comments, variables, functions, etc. different colors as per modern IDEs?

Claude added syntax highlighting via a colorized overlay behind a transparent textarea (monospace-aligned so the caret lands correctly). A follow-up refined the intent — the editor should be plain-weight but colorized, "similar to how latex is in plain text but rendered in many fonts", with boldface/italic conventions reserved for the GUI panels. Then:

Terence:

Since we added IDE-like colorization to the program, we can also add IDE-type hovertext. Hovering over a function or reserved word should give a description of it; hovering over a random variable or constant will reveal its definition.

Claude added hover tooltips. Terence suggested a simplification — "if one maintains a table (JSON etc.) of definitions of terms, they can also be reused for the documentation portion of the page" — so Claude made a single in-memory GLOSSARY the source of truth for both the hover tips and the generated reference tables, so the two cannot drift apart. A design note from Terence framed this work: the aim is to "evoke familiar interfaces and design choices from other tools a student could plausibly be familiar with (IDEs, LaTeX, YouTube, etc.)".

Presets grew to cover each feature, and — once the machinery allowed it — a set of classic paradoxes: Monty Hall, Berkson's paradox, the base-rate fallacy, St. Petersburg, and (after two small new functions) the birthday and Simpson's paradoxes.


8. Events, and a Boolean type

Events were added as a first-class object, as the prerequisite for universes.

Terence:

Maybe one can introduce events as a new object class, alongside random variables, so one can set E = X > 3 and later do things like (Y | E) […]. While random variables are displayed using mean and stddev, events can be displayed as True/False with an empirical probability. […] After writing this out, it seems maybe the thing to do is to first implement events before moving on to universes.

Claude agreed that events were the right foundation and asked two questions; Terence chose word operators (and / or / not, over symbols) and that a bare comparison such as X > 3 should itself be an event (displaying True/False, coercing to 0/1 in arithmetic). Further requests followed during the build:

Aliases are cheap, let's have lots of aliases. […] We should at some point implement a guard against using a variable that is a reserved word or a function […] it is good programming practice to teach students to not do this.

Events can come with a constructor, maybe Boolean(p) […] giving a new independent event that is true with probability p. […] We can set p=1/2 as the default value […]; Normal might default to Normal(0,1).

In the spirit of aliasing, we can have both Excel-style boolean operators and Python-style operators, so we can have AND(,,,.) […]; conversely we can implement if … then … else.

Claude built the event type (comparisons, boolean algebra, the Boolean(p) coin with aliases), a reserved-name guard, default distribution parameters, True/False literals, and the boolean connectives in three interchangeable spellings — words, symbols (&&, ||, !), and Excel functions (AND/OR/NOT/XOR) — plus the if C then A else B word conditional. Events render as True/False with a live probability; a plot of an event uses a False/True axis (Terence noted the earlier 0..1 axis "suggests an implementation of fuzzy logic which is definitely out of scope").

9. Universes

The (X | E) feature from message [2] was implemented next. Terence opened with a design.

Terence:

My tentative plan for universes is as a sort of tree: there is a root universe, and new universes can be created by conditioning on events E from a prior universe. […] one can manipulate random variables in other universes so long as one does not mix together different universes. […] We can enforce exact syntactic identity […] on whether two events E, E' are "equal" for the purpose of determining whether one is in the same universe.

Claude proposed an implementation and, initially, that conditioning could be treated as commutative (an unordered set of events). Terence noted a problem:

I'm not completely sure that conditioning commutes when one starts creating conditionally independent samples. Suppose […] one first conditions to an event E, then creates two independent samples X, Y […] and then conditions on a further event, say X+Y > 7. One can't really shuffle the two conditionings around. So a tree structure may be necessary.

Once sample-creation is interleaved, conditioning steps do not freely commute, so the design used an ordered tree of universes. Claude built Stage B1: (X | E) parses to a given node that opens a child universe; each universe rejection-samples its own ω, so a conditional is independent of the original and coupled to same-universe siblings; mixing quantities from different universes is a compile-time error. The Values panel splits by universe and each plot shows a | E breadcrumb. A side effect useful for the paradox presets is that (·|E) allows a quantity to be plotted before and after conditioning, side by side (the Two-children and Berkson presets were rebuilt to show this). Terence restated the scope: "the design philosophy here is not to gain Turing completeness or maximal expressivity, but merely enough expressivity to describe many common probabilistic contexts using readable code."


10. Error handling — no silent failures

Terence:

The error handling on plot seems to be silent. If I ask it to plot something other than a scalar variable, it seems to default to the first variable it can find, rather than reporting a syntax error. […] more generally this might be a good time to review all commands to make sure they do not have a silent failure mode but instead report syntax errors. And add some syntax error checks to the unit tests.

Claude made invalid plot targets (unknown / constant / vector / non-name / too many / cross-universe) into reported compile errors, audited the other commands for silent fallbacks, and added a block of syntax-error tests. Terence then asked to extend the audit:

Some of these audits might also be worth running on the relativity and Gilbreath apps. (And we can add to the skill that such checks should be a routine part of the applet development process.)

Claude audited the other two original apps — fixing one silent failure in the spacetime app (observer coordinates were not checked for finiteness), while the Gilbreath app was already clean — and recorded "no silent failure modes" and "docs stay faithful to the model" as routine applet-development checks in the skill.

11. Statistics sugar, and freezing the syntax

Terence (noticing the header now over-claimed):

The header text asserts "there is one shared omega per tick", but this is now only true in the unconditional universe. Maybe a disclaimer such as "In the absence of any conditioning" may clarify.

Terence added syntactic sugar in stages — conditional statistics Cov(X, Y | E), a P(E) probability statistic, and arithmetic on statistics (Max(X) - Min(X)) — each time restating a type constraint: "I view statistics as a separate class of object from either constants or random variables, and think they should not mingle." One consequence: because statistics live only inside the statistic command, they did not need to be reserved words, which freed names like p and sd for variables, so adding P(E) did not remove p.

Terence also asked whether the syntax was complete:

In your opinion is there any reason to otherwise change the core syntax, keeping in mind the design philosophy of being readable and to resemble existing languages such as Excel and Python? […] readability takes priority over Turing completeness or other measures of expressive power.

The response was that no further core changes were needed once the sugar was in, and that the one purely additive option was a ** alias for ^ (Python spelling of power), which Terence took. The "Out of scope (by design)" section was made explicit: no general-purpose programming (loops, user-defined functions, recursion, mutable state), no random matrices, no conditioning on a random variable, no 3-D — with a pointer to MATLAB / R / NumPy for those. The matrix idea from message [3] was reassessed as a larger undertaking than first estimated (complex numbers, an eigenvalue renderer, a point-process type, gap distributions) rather than a small addition.


Summary

The target stated in the first message did not change: a small, readable language in which a random variable is a quantity that fluctuates as its randomness is resampled, aimed at a probability student rather than a programmer. Features were added roughly in the order a course might introduce them — coupling and independence, conditioning, empirical statistics, events and their algebra, and conditional "universes". A fair amount of the later work concerned what to exclude (the "Out of scope" section) as much as what to add. The (X | E) construct raised in the first design discussion was among the last features built.

Built with Claude Code. The engine (random-variables.js) is DOM-free and covered by ~160 headless unit tests plus a DOM-shim smoke test; the app (random-variables.html) runs entirely in the browser. Suggestions from the 2016 post's commenters are credited in the footer.