Repository navigation
Speculative Plotting
Speculative design. Open, not settled — thinking and proposals, not shipped behavior. See Roadmap for what's in flight.
Status: slice approved (Dean, 2026-11-12, with every lean in §5); building (BL-396). A survey of what notatio draws, the gap to
Wolfram's visualization functions (and Sage's where it adds something), and one slice to build
first: field plots drawn per pixel, which adds RegionPlot and implicit curves and makes
DensityPlot and ContourPlot exact at every zoom. Nothing here adds a math head; plots are
held heads in @notatio/formats, lowered in frontend, drawn in components.
| family | heads | sampled by | drawn as |
|---|---|---|---|
| curves |
Plot, ParametricPlot, PolarPlot, ListPlot, ListLinePlot, ListPolarPlot, DiscretePlot
|
compiled JavaScript; adaptive bisection on bend and on a finite/non-finite change (adaptiveSample, 41 points, depth 6, cap 4000); resampled over the window on zoom |
SVG LineBox in a GraphicsBox
|
| fields |
ContourPlot, DensityPlot, ListContourPlot, ListDensityPlot
|
compiled JavaScript on a fixed grid (40 or 48 a side, capped 120 and 160) | marching-squares LineBox per level, or one RasterBox; no pan or zoom |
| vector fields |
VectorPlot, StreamPlot
|
compiled JavaScript on cell centers; fixed-step RK4 streamlines |
ArrowBox, LineBox
|
| surfaces |
Plot3D, ListPlot3D, BarChart3D, Graphics3D(Line)
|
grid 26 a side (to 60); Gpu -> n evaluates the grid in a fragment shader (to 400) and reads it back |
Graphics3DBox, painter's algorithm to SVG or a 2-D canvas |
| complex |
ComplexPlot, ComplexPlot3D
|
emitComplexGLSL, one fragment per pixel; literals in uniform slots, so a slider never relinks; a CPU twin as fallback |
WebGL2 canvas, pan and zoom by uniform |
| charts |
BarChart, Histogram, PieChart, BoxWhiskerChart, ArrayPlot
|
data | GraphicsBox |
| layers |
Show, the diagram layers, LatticeTiles with ColorRules, ColorMixing, BoundaryStyle
|
per view |
GraphicsBox, canvas blending |
Options that reach a sampler or a drawer today: PlotLabel, GridLines, PlotLegends (a
boolean), Filling (a boolean), ColorFunction (a gradient name, or "x"/"y"), PlotPoints,
AxesLabel, PlotRange (the y pair on Plot, the z pair on DensityPlot), ScalingFunctions,
Epilog/Prolog, and on Show the frame options (AspectRatio, Axes, Ticks, the camera).
Field plots also read three names that are not Wolfram's: Levels, Filled, N.
Every plot example states a boxes shape (about 45 rows), held by
packages/frontend/tests/example-boxes.test.ts, which lowers in Node with the page's own
samplers. Shaders run only in the Playwright GPU stories (web/tests/stories/gpu.spec.ts).
Two routes compile an expression for the GPU. The complex one (emitComplexGLSL, ours) hoists
literals into uniforms and has a CPU mirror. The real one (toGlslFn, compute-engine's
GLSLTarget) returns float plotFn(float x, float y); it is used only to fill a grid and read it
back, it bakes a Manipulate value into the source (each slider move relinks), and it declines
loops.
Value is to a reader of the docs and a notebook user; cost is relative (S about a day of builder time, L a design of its own).
| # | gap | value | cost | GPU |
|---|---|---|---|---|
| 1 |
Fields per pixel: DensityPlot/ContourPlot are a 40-cell grid with no zoom; no RegionPlot; no implicit curve (ContourPlot(f == g), Sage's implicit_plot); no RegionFunction; Contours/ContourShading/PlotPoints spelled Levels/Filled/N
|
high: inequalities and implicit curves are the most-missed plots, and every field page gains pan, zoom and sharp edges | M | is the point: one fragment per pixel, as the portrait already does |
| 2 |
Curve options: Exclusions (none; a pole is found only by a finite/non-finite change or a jump wider than the window), MaxRecursion (fixed at 6), PlotStyle, ClippingStyle, PlotRange in full (All, Automatic, both axes), Filling -> Bottom | Axis | value | {1 -> {2}}, PlotLegends with labels, Legended
|
high for correctness (a Tan or Floor plot still draws a join where the samples straddle it), medium for looks |
M | no. compute-engine's interval-js target can prove a segment continuous or locate the break |
| 3 |
3-D beyond a height field: ParametricPlot3D, RevolutionPlot3D, SphericalPlot3D, ContourPlot3D, RegionPlot3D, ListPointPlot3D
|
high (tori, spheres, knots as tubes) | L: a self-intersecting surface breaks the painter's algorithm, so it needs a depth-buffered WebGL drawer for Graphics3DBox first |
yes: the drawer, and marching the implicit ones |
| 4 |
Surface options: Mesh, MeshFunctions, MeshShading, RegionFunction, BoxRatios, PlotRange clipping, ColorFunction as an expression, lighting |
medium | M once 3's drawer exists; S for RegionFunction alone |
follows 3 |
| 5 |
Vector fields, deeper: VectorScaling/VectorPoints/StreamPoints/StreamScale, VectorColorFunction, ListVectorPlot, ListStreamPlot, StreamDensityPlot, LineIntegralConvolutionPlot; Sage's plot_slope_field
|
medium | S for options and the list forms; M for the dense ones | LIC and stream density are per-pixel shaders over two plotFns |
| 6 |
Graphics as values: a 2-D Graphics(primitives) head, Legended, Labeled around a plot, GraphicsRow/GraphicsGrid, Inset
|
medium; it is the Figures and Speculative-Box-Primitives agenda | M | no |
| 7 |
Named scale forms (LogPlot, LogLogPlot, LogLinearPlot, ListLogPlot): ScalingFunctions already does the work |
low | S | no |
| 8 |
Plot themes and frames: PlotTheme, Frame/FrameLabel/FrameTicks on Plot, PlotRangePadding, AxesOrigin
|
low to medium | S each | no |
| 9 |
Data plots: ListPlot with Joined, PlotMarkers, ErrorBar; DateListPlot, SmoothHistogram, BubbleChart
|
low for a math site | S to M | no |
Two candidates named when this lane opened are already built: VectorPlot and StreamPlot
exist (on the CPU, which is right for arrows and polylines), and Plot already samples
adaptively. What is missing next to them is RegionPlot, and Exclusions.
Not in this lane: BL-329 (the notebook's shared range for Plot and Plot3D, an open design
call). BL-322's plot fixes (explicit ranges, square field plots) merged as #1213, so no lane
holds the plot files. A change to field output may need the cli vector-plot golden regenerated
(UPDATE_*), coordinated with B's cli test lane.
One fragment shader host for a real f(x, y), the way portraitShader is for a complex f(z),
and a CPU twin that draws the same picture where there is no shader. Four heads share it.
RegionPlot(x^2 + y^2 < 4 && y > Sin(x), (x, -3, 3), (y, -3, 3))
ContourPlot(x^3 + y^3 == 3 x y, (x, -3, 3), (y, -3, 3)) // an implicit curve
ContourPlot(Sin(x) Cos(y), (x, 0, 6), (y, 0, 6), Contours -> 12, ContourShading -> True)
DensityPlot(Sin(x y), (x, -3, 3), (y, -3, 3), RegionFunction -> (x^2 + y^2 < 9), ColorFunction -> "Dusk")
Show(RegionPlot(y > x^2, (x, -2, 2), (y, -1, 4)), Plot(x^2, (x, -2, 2)))
A predicate becomes one real function, positive inside:
| predicate | margin |
|---|---|
a < b, a <= b
|
b - a |
a > b, a >= b
|
a - b |
p && q |
min(p, q) |
p || q |
max(p, q) |
!p |
-p |
a == b |
the curve a - b = 0 (a contour, not a region) |
a chain a < b < c
|
min(b - a, c - b) |
marginOf(expr) is one pure function in frontend (field-margin.ts). Both routes compile its
result with the ordinary real compile (compilePlot), so the region the shader fills and the
boundary the CPU marches are the same function, and nothing depends on how a compile target
treats booleans. A predicate it cannot rewrite (Element(x, Integers), a quantifier) declines:
the plot says so and draws nothing.
-
RegionPlotfillsmargin > 0and strokesmargin = 0(BoundaryStyle;PlotStylethe fill). A list of predicates is one region each, colored in order, mixed where they overlap byColorMixing(ours, as on the layers; default"Normal"with Wolfram's translucent fill). -
ContourPlot(f == g, …)is the zero contour off - g: Wolfram's form, and what Sage callsimplicit_plot. A list of equations is a curve each. -
RegionFunction -> predon any field plot is a margin too: outside it nothing is drawn. Its body is written over the plot's own variables, as the examples show (Wolfram's pure functionFunction({x, y, z}, …)is accepted and applied).
Strict and non-strict inequalities draw alike (Wolfram dashes nothing either).
fieldShader(kind, plotFns) (frontend/src/field-shader.ts) is built like portraitShader: the
same Prm uniform block (center, resolution, extent, literal slots, gradient stops), the same
gradient(t), the same pan and zoom by uniform, the same ComplexPlotRenderer lifecycle
(generalized into a shared renderer rather than copied). Per pixel it evaluates plotFn and
colors by kind:
-
density:
gradient((f - z0) / (z1 - z0)),z0..z1fromPlotRangeor, when automatic, from a coarse CPU grid over the view (the 2nd to 98th percentile, as curves do). -
contours: band index
floorover the levels into the gradient whenContourShading, and a line where|f - level| < w · |∇f|, with∇ffromdFdx/dFdy, so a line iswpixels wide at every zoom and slope. Levels ride in a uniform array (MAX_LEVELS32; more declines to the CPU).Contours -> npicksnround levels over the view's range,Contours -> [v…]exact ones. -
region:
smoothstepofmargin / |∇margin|for an antialiased fill, and the same line rule at 0 for the boundary. -
masks: a non-finite
fis transparent; outsideRegionFunctionis transparent.
Numerics, by the house rule (decline rather than answer wrong):
- f32 only. The view's extent is clamped where a pixel step falls under
ulp(center), as the portrait clamps its zoom; past it the plot stops zooming rather than drawing blocks. - Where
|∇f|underflows (a flat region at exactly a level) no line is drawn; the band color still is. -
toGlslFndeclining (a loop,Random, a head with no GLSL name) sends the plot to the CPU twin.
Literal slots for the real route. Today a Manipulate value is baked into plotFn, so a
slider relinks the program on every move. The host binds each wildcard to a uniform slot instead
(prm.p[i].x), compiled as a free symbol whose GLSL name is the slot. If compute-engine's
GLSLTarget cannot name a symbol that way without a change, the slice keeps baking (programs are
already cached per source) and the ask goes to the CE upstream session; nothing in ce-patches
is touched here.
The value of a field plot stays a GraphicsBox, and Node, print, the terminal and a page with no
WebGL2 all get it from the CPU:
RegionPlot(…) → GraphicsBox(RasterBox(mask), LineBox(boundary)…, PlotRange -> …, Frame -> True)
ContourPlot(…) → GraphicsBox(LineBox per level…) // or RasterBox when ContourShading
ContourPlot(f == g, …) → GraphicsBox(LineBox…) // roles: Series
DensityPlot(…) → GraphicsBox(RasterBox(cells), …)
That is today's lowering (fieldBoxOfGrid, marchingSquares), plus the region mask and
RegionFunction. On the web, <graphics-box> draws the same box's field layer with the shader
when it can: a canvas under the SVG frame, in the frame's shared view, so Show(RegionPlot, Plot)
overlays and pans as one (Figures, "Frames and views"). This is decision 9 of
Speculative-Box-Primitives (a producer keyed by view inside GraphicsBox) for fields: the
box holds the expression (Producer -> head, as an unsampled plot does now), the shader is one
producer, the grid another, and reduce pins the view for static environments.
The CPU grid also gets better on its own: on zoom it resamples over the window (fields are
#unzoomable today), and marching squares refines a cell where the margin changes sign more than
once, so a boundary is not lost between samples.
Each head's record declares its options (Figures, "Options and heads"); all are Rules.
| option | heads | notes |
|---|---|---|
Contours -> n | [v…] |
ContourPlot, ListContourPlot
|
replaces Levels (hard rename, no alias) |
ContourShading -> True | False |
same | replaces Filled; default False stays (ours; Wolfram's is Automatic, shaded) — a booleans-default-False call for Dean, see §5 |
PlotPoints -> n |
all fields | replaces N; the CPU grid only, the shader ignores it |
RegionFunction -> pred |
all fields, then Plot3D in a later slice |
§3.1 |
PlotRange -> [z0, z1] | All | Automatic |
DensityPlot, ContourPlot
|
already on DensityPlot
|
ColorFunction -> name, PlotLegends, PlotLabel, FrameLabel
|
all fields | as today (XLabel/YLabel become FrameLabel -> [x, y]) |
PlotStyle, BoundaryStyle
|
RegionPlot |
a color, Opacity(a, color), None; ColorRules is for addressed elements, a region has none |
ContourStyle |
ContourPlot |
line color and AbsoluteThickness
|
ColorMixing |
RegionPlot of a list |
ours |
-
formats/reference/RegionPlot/is new (index.md,examples.tsv): a disk, a conjunction, a list of two regions, aRegionFunction, a declined predicate.ContourPlotgains the implicit curve andContours/ContourShadingrows;DensityPlotaRegionFunctionrow. Each statesboxes({head: GraphicsBox, marks: {RasterBox: 1, LineBox: 1}, roles: {Region: 1, Boundary: 1}}), held in Node byexample-boxes.test.tsthrough the CPU twin. No pixel goldens. -
marginOfhas unit tests over the table in §3.1, and a cross-check that the sign of the compiled margin agrees withevaluate()of the predicate on a grid (the routes agree). - The shader's constants and coloring are shared with the CPU painter from one table, and a Node
test holds them equal, as
portrait-cpu.test.tsdoes for the portrait. -
web/tests/stories/gpu.spec.tsgains behavior checks read from pixels: a disk's filled share is π r² of the frame, a region and its complement tile it, an implicit circle's line has the radius it should after a zoom, a slider moves a boundary without a relink.
- new:
frontend/src/field-margin.ts,field-shader.ts,field-cpu.ts;formats/reference/RegionPlot/ - changed:
frontend/src/{contour,densityplot,plot-lowering,plot-notation,symbols,plot-compile,complex-plot,gpu-eval}.ts,frontend/scripts/example-boxes.ts,components/src/{plot-view,graphics-box}.ts,formats/src/{graphics,graphics-options}.ts,formats/reference/{ContourPlot,DensityPlot,ListContourPlot,ListDensityPlot}/, docs that useLevels/Filled/N,web/tests/stories/gpu.spec.ts - untouched:
packages/boxes(no new box head:RasterBoxandLineBoxcarry it),packages/ce-patches,packages/engine, the complex emitter.
Two PRs: (a) margins, RegionPlot, implicit curves, RegionFunction and the option renames on
the CPU route, with the records; (b) the shader host, the shared view, slots and the GPU stories.
-
Curve options (row 2):
Exclusions -> Automatic | None | [x == a, …], whereAutomaticasks theinterval-jscompile of the expression whether a segment's image is bounded, bisects to the break, and never joins across it;MaxRecursion,ClippingStyle,PlotStyle,Fillingin full, labeledPlotLegends. -
A depth-buffered drawer for
Graphics3DBox(rows 3 and 4): thenParametricPlot3D,RevolutionPlot3D,SphericalPlot3D,Mesh/MeshFunctions, andContourPlot3Dby marching the field host'splotFnin 3-D. -
Dense vector fields (row 5) on the field host:
StreamDensityPlot, line integral convolution.
- Fields per pixel (§3) first, in two PRs, before curve options and 3-D.
- Hard renames, no aliases:
Levels→Contours,Filled→ContourShading,N→PlotPointson the field plots;FrameLabel -> [x, y]captions their frame. -
ContourShadingdefaults toFalse(lines), by the booleans-default-Falserule. -
RegionFunction -> (x^2 + y^2 < 9), a predicate over the plot's variables, is the primary spelling; aFunctionis accepted. - Overlapping regions of
RegionPlot([p, q], …)are translucent and painted each over the last (ColorMixing -> "Normal") unlessColorMixingsays otherwise.